actioncable 6.0.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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +169 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +24 -0
  5. data/app/assets/javascripts/action_cable.js +517 -0
  6. data/lib/action_cable.rb +62 -0
  7. data/lib/action_cable/channel.rb +17 -0
  8. data/lib/action_cable/channel/base.rb +311 -0
  9. data/lib/action_cable/channel/broadcasting.rb +41 -0
  10. data/lib/action_cable/channel/callbacks.rb +37 -0
  11. data/lib/action_cable/channel/naming.rb +25 -0
  12. data/lib/action_cable/channel/periodic_timers.rb +78 -0
  13. data/lib/action_cable/channel/streams.rb +176 -0
  14. data/lib/action_cable/channel/test_case.rb +310 -0
  15. data/lib/action_cable/connection.rb +22 -0
  16. data/lib/action_cable/connection/authorization.rb +15 -0
  17. data/lib/action_cable/connection/base.rb +264 -0
  18. data/lib/action_cable/connection/client_socket.rb +157 -0
  19. data/lib/action_cable/connection/identification.rb +47 -0
  20. data/lib/action_cable/connection/internal_channel.rb +45 -0
  21. data/lib/action_cable/connection/message_buffer.rb +54 -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 +79 -0
  25. data/lib/action_cable/connection/tagged_logger_proxy.rb +42 -0
  26. data/lib/action_cable/connection/test_case.rb +234 -0
  27. data/lib/action_cable/connection/web_socket.rb +41 -0
  28. data/lib/action_cable/engine.rb +79 -0
  29. data/lib/action_cable/gem_version.rb +17 -0
  30. data/lib/action_cable/helpers/action_cable_helper.rb +42 -0
  31. data/lib/action_cable/remote_connections.rb +71 -0
  32. data/lib/action_cable/server.rb +17 -0
  33. data/lib/action_cable/server/base.rb +94 -0
  34. data/lib/action_cable/server/broadcasting.rb +54 -0
  35. data/lib/action_cable/server/configuration.rb +56 -0
  36. data/lib/action_cable/server/connections.rb +36 -0
  37. data/lib/action_cable/server/worker.rb +75 -0
  38. data/lib/action_cable/server/worker/active_record_connection_management.rb +21 -0
  39. data/lib/action_cable/subscription_adapter.rb +12 -0
  40. data/lib/action_cable/subscription_adapter/async.rb +29 -0
  41. data/lib/action_cable/subscription_adapter/base.rb +30 -0
  42. data/lib/action_cable/subscription_adapter/channel_prefix.rb +28 -0
  43. data/lib/action_cable/subscription_adapter/inline.rb +37 -0
  44. data/lib/action_cable/subscription_adapter/postgresql.rb +132 -0
  45. data/lib/action_cable/subscription_adapter/redis.rb +181 -0
  46. data/lib/action_cable/subscription_adapter/subscriber_map.rb +59 -0
  47. data/lib/action_cable/subscription_adapter/test.rb +40 -0
  48. data/lib/action_cable/test_case.rb +11 -0
  49. data/lib/action_cable/test_helper.rb +133 -0
  50. data/lib/action_cable/version.rb +10 -0
  51. data/lib/rails/generators/channel/USAGE +13 -0
  52. data/lib/rails/generators/channel/channel_generator.rb +52 -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 +5 -0
  59. data/lib/rails/generators/test_unit/channel_generator.rb +20 -0
  60. data/lib/rails/generators/test_unit/templates/channel_test.rb.tt +8 -0
  61. metadata +149 -0
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionCable
4
+ module Connection
5
+ # Allows the use of per-connection tags against the server logger. This wouldn't work using the traditional
6
+ # <tt>ActiveSupport::TaggedLogging</tt> enhanced Rails.logger, as that logger will reset the tags between requests.
7
+ # The connection is long-lived, so it needs its own set of tags for its independent duration.
8
+ class TaggedLoggerProxy
9
+ attr_reader :tags
10
+
11
+ def initialize(logger, tags:)
12
+ @logger = logger
13
+ @tags = tags.flatten
14
+ end
15
+
16
+ def add_tags(*tags)
17
+ @tags += tags.flatten
18
+ @tags = @tags.uniq
19
+ end
20
+
21
+ def tag(logger)
22
+ if logger.respond_to?(:tagged)
23
+ current_tags = tags - logger.formatter.current_tags
24
+ logger.tagged(*current_tags) { yield }
25
+ else
26
+ yield
27
+ end
28
+ end
29
+
30
+ %i( debug info warn error fatal unknown ).each do |severity|
31
+ define_method(severity) do |message|
32
+ log severity, message
33
+ end
34
+ end
35
+
36
+ private
37
+ def log(type, message) # :doc:
38
+ tag(@logger) { @logger.send type, message }
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+ require "active_support/test_case"
5
+ require "active_support/core_ext/hash/indifferent_access"
6
+ require "action_dispatch"
7
+ require "action_dispatch/http/headers"
8
+ require "action_dispatch/testing/test_request"
9
+
10
+ module ActionCable
11
+ module Connection
12
+ class NonInferrableConnectionError < ::StandardError
13
+ def initialize(name)
14
+ super "Unable to determine the connection to test from #{name}. " +
15
+ "You'll need to specify it using `tests YourConnection` in your " +
16
+ "test case definition."
17
+ end
18
+ end
19
+
20
+ module Assertions
21
+ # Asserts that the connection is rejected (via +reject_unauthorized_connection+).
22
+ #
23
+ # # Asserts that connection without user_id fails
24
+ # assert_reject_connection { connect params: { user_id: '' } }
25
+ def assert_reject_connection(&block)
26
+ assert_raises(Authorization::UnauthorizedError, "Expected to reject connection but no rejection was made", &block)
27
+ end
28
+ end
29
+
30
+ # We don't want to use the whole "encryption stack" for connection
31
+ # unit-tests, but we want to make sure that users test against the correct types
32
+ # of cookies (i.e. signed or encrypted or plain)
33
+ class TestCookieJar < ActiveSupport::HashWithIndifferentAccess
34
+ def signed
35
+ self[:signed] ||= {}.with_indifferent_access
36
+ end
37
+
38
+ def encrypted
39
+ self[:encrypted] ||= {}.with_indifferent_access
40
+ end
41
+ end
42
+
43
+ class TestRequest < ActionDispatch::TestRequest
44
+ attr_accessor :session, :cookie_jar
45
+ end
46
+
47
+ module TestConnection
48
+ attr_reader :logger, :request
49
+
50
+ def initialize(request)
51
+ inner_logger = ActiveSupport::Logger.new(StringIO.new)
52
+ tagged_logging = ActiveSupport::TaggedLogging.new(inner_logger)
53
+ @logger = ActionCable::Connection::TaggedLoggerProxy.new(tagged_logging, tags: [])
54
+ @request = request
55
+ @env = request.env
56
+ end
57
+ end
58
+
59
+ # Unit test Action Cable connections.
60
+ #
61
+ # Useful to check whether a connection's +identified_by+ gets assigned properly
62
+ # and that any improper connection requests are rejected.
63
+ #
64
+ # == Basic example
65
+ #
66
+ # Unit tests are written as follows:
67
+ #
68
+ # 1. Simulate a connection attempt by calling +connect+.
69
+ # 2. Assert state, e.g. identifiers, has been assigned.
70
+ #
71
+ #
72
+ # class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
73
+ # def test_connects_with_proper_cookie
74
+ # # Simulate the connection request with a cookie.
75
+ # cookies["user_id"] = users(:john).id
76
+ #
77
+ # connect
78
+ #
79
+ # # Assert the connection identifier matches the fixture.
80
+ # assert_equal users(:john).id, connection.user.id
81
+ # end
82
+ #
83
+ # def test_rejects_connection_without_proper_cookie
84
+ # assert_reject_connection { connect }
85
+ # end
86
+ # end
87
+ #
88
+ # +connect+ accepts additional information the HTTP request with the
89
+ # +params+, +headers+, +session+ and Rack +env+ options.
90
+ #
91
+ # def test_connect_with_headers_and_query_string
92
+ # connect params: { user_id: 1 }, headers: { "X-API-TOKEN" => "secret-my" }
93
+ #
94
+ # assert_equal "1", connection.user.id
95
+ # assert_equal "secret-my", connection.token
96
+ # end
97
+ #
98
+ # def test_connect_with_params
99
+ # connect params: { user_id: 1 }
100
+ #
101
+ # assert_equal "1", connection.user.id
102
+ # end
103
+ #
104
+ # You can also setup the correct cookies before the connection request:
105
+ #
106
+ # def test_connect_with_cookies
107
+ # # Plain cookies:
108
+ # cookies["user_id"] = 1
109
+ #
110
+ # # Or signed/encrypted:
111
+ # # cookies.signed["user_id"] = 1
112
+ # # cookies.encrypted["user_id"] = 1
113
+ #
114
+ # connect
115
+ #
116
+ # assert_equal "1", connection.user_id
117
+ # end
118
+ #
119
+ # == Connection is automatically inferred
120
+ #
121
+ # ActionCable::Connection::TestCase will automatically infer the connection under test
122
+ # from the test class name. If the channel cannot be inferred from the test
123
+ # class name, you can explicitly set it with +tests+.
124
+ #
125
+ # class ConnectionTest < ActionCable::Connection::TestCase
126
+ # tests ApplicationCable::Connection
127
+ # end
128
+ #
129
+ class TestCase < ActiveSupport::TestCase
130
+ module Behavior
131
+ extend ActiveSupport::Concern
132
+
133
+ DEFAULT_PATH = "/cable"
134
+
135
+ include ActiveSupport::Testing::ConstantLookup
136
+ include Assertions
137
+
138
+ included do
139
+ class_attribute :_connection_class
140
+
141
+ attr_reader :connection
142
+
143
+ ActiveSupport.run_load_hooks(:action_cable_connection_test_case, self)
144
+ end
145
+
146
+ module ClassMethods
147
+ def tests(connection)
148
+ case connection
149
+ when String, Symbol
150
+ self._connection_class = connection.to_s.camelize.constantize
151
+ when Module
152
+ self._connection_class = connection
153
+ else
154
+ raise NonInferrableConnectionError.new(connection)
155
+ end
156
+ end
157
+
158
+ def connection_class
159
+ if connection = self._connection_class
160
+ connection
161
+ else
162
+ tests determine_default_connection(name)
163
+ end
164
+ end
165
+
166
+ def determine_default_connection(name)
167
+ connection = determine_constant_from_test_name(name) do |constant|
168
+ Class === constant && constant < ActionCable::Connection::Base
169
+ end
170
+ raise NonInferrableConnectionError.new(name) if connection.nil?
171
+ connection
172
+ end
173
+ end
174
+
175
+ # Performs connection attempt to exert #connect on the connection under test.
176
+ #
177
+ # Accepts request path as the first argument and the following request options:
178
+ #
179
+ # - params – URL parameters (Hash)
180
+ # - headers – request headers (Hash)
181
+ # - session – session data (Hash)
182
+ # - env – additional Rack env configuration (Hash)
183
+ def connect(path = ActionCable.server.config.mount_path, **request_params)
184
+ path ||= DEFAULT_PATH
185
+
186
+ connection = self.class.connection_class.allocate
187
+ connection.singleton_class.include(TestConnection)
188
+ connection.send(:initialize, build_test_request(path, request_params))
189
+ connection.connect if connection.respond_to?(:connect)
190
+
191
+ # Only set instance variable if connected successfully
192
+ @connection = connection
193
+ end
194
+
195
+ # Exert #disconnect on the connection under test.
196
+ def disconnect
197
+ raise "Must be connected!" if connection.nil?
198
+
199
+ connection.disconnect if connection.respond_to?(:disconnect)
200
+ @connection = nil
201
+ end
202
+
203
+ def cookies
204
+ @cookie_jar ||= TestCookieJar.new
205
+ end
206
+
207
+ private
208
+ def build_test_request(path, params: nil, headers: {}, session: {}, env: {})
209
+ wrapped_headers = ActionDispatch::Http::Headers.from_hash(headers)
210
+
211
+ uri = URI.parse(path)
212
+
213
+ query_string = params.nil? ? uri.query : params.to_query
214
+
215
+ request_env = {
216
+ "QUERY_STRING" => query_string,
217
+ "PATH_INFO" => uri.path
218
+ }.merge(env)
219
+
220
+ if wrapped_headers.present?
221
+ ActionDispatch::Http::Headers.from_hash(request_env).merge!(wrapped_headers)
222
+ end
223
+
224
+ TestRequest.create(request_env).tap do |request|
225
+ request.session = session.with_indifferent_access
226
+ request.cookie_jar = cookies
227
+ end
228
+ end
229
+ end
230
+
231
+ include Behavior
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "websocket/driver"
4
+
5
+ module ActionCable
6
+ module Connection
7
+ # Wrap the real socket to minimize the externally-presented API
8
+ class WebSocket # :nodoc:
9
+ def initialize(env, event_target, event_loop, protocols: ActionCable::INTERNAL[:protocols])
10
+ @websocket = ::WebSocket::Driver.websocket?(env) ? ClientSocket.new(env, event_target, event_loop, protocols) : nil
11
+ end
12
+
13
+ def possible?
14
+ websocket
15
+ end
16
+
17
+ def alive?
18
+ websocket && websocket.alive?
19
+ end
20
+
21
+ def transmit(data)
22
+ websocket.transmit data
23
+ end
24
+
25
+ def close
26
+ websocket.close
27
+ end
28
+
29
+ def protocol
30
+ websocket.protocol
31
+ end
32
+
33
+ def rack_response
34
+ websocket.rack_response
35
+ end
36
+
37
+ private
38
+ attr_reader :websocket
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+ require "action_cable"
5
+ require "action_cable/helpers/action_cable_helper"
6
+ require "active_support/core_ext/hash/indifferent_access"
7
+
8
+ module ActionCable
9
+ class Engine < Rails::Engine # :nodoc:
10
+ config.action_cable = ActiveSupport::OrderedOptions.new
11
+ config.action_cable.mount_path = ActionCable::INTERNAL[:default_mount_path]
12
+
13
+ config.eager_load_namespaces << ActionCable
14
+
15
+ initializer "action_cable.helpers" do
16
+ ActiveSupport.on_load(:action_view) do
17
+ include ActionCable::Helpers::ActionCableHelper
18
+ end
19
+ end
20
+
21
+ initializer "action_cable.logger" do
22
+ ActiveSupport.on_load(:action_cable) { self.logger ||= ::Rails.logger }
23
+ end
24
+
25
+ initializer "action_cable.set_configs" do |app|
26
+ options = app.config.action_cable
27
+ options.allowed_request_origins ||= /https?:\/\/localhost:\d+/ if ::Rails.env.development?
28
+
29
+ app.paths.add "config/cable", with: "config/cable.yml"
30
+
31
+ ActiveSupport.on_load(:action_cable) do
32
+ if (config_path = Pathname.new(app.config.paths["config/cable"].first)).exist?
33
+ self.cable = Rails.application.config_for(config_path).with_indifferent_access
34
+ end
35
+
36
+ previous_connection_class = connection_class
37
+ self.connection_class = -> { "ApplicationCable::Connection".safe_constantize || previous_connection_class.call }
38
+
39
+ options.each { |k, v| send("#{k}=", v) }
40
+ end
41
+ end
42
+
43
+ initializer "action_cable.routes" do
44
+ config.after_initialize do |app|
45
+ config = app.config
46
+ unless config.action_cable.mount_path.nil?
47
+ app.routes.prepend do
48
+ mount ActionCable.server => config.action_cable.mount_path, internal: true
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ initializer "action_cable.set_work_hooks" do |app|
55
+ ActiveSupport.on_load(:action_cable) do
56
+ ActionCable::Server::Worker.set_callback :work, :around, prepend: true do |_, inner|
57
+ app.executor.wrap do
58
+ # If we took a while to get the lock, we may have been halted
59
+ # in the meantime. As we haven't started doing any real work
60
+ # yet, we should pretend that we never made it off the queue.
61
+ unless stopping?
62
+ inner.call
63
+ end
64
+ end
65
+ end
66
+
67
+ wrap = lambda do |_, inner|
68
+ app.executor.wrap(&inner)
69
+ end
70
+ ActionCable::Channel::Base.set_callback :subscribe, :around, prepend: true, &wrap
71
+ ActionCable::Channel::Base.set_callback :unsubscribe, :around, prepend: true, &wrap
72
+
73
+ app.reloader.before_class_unload do
74
+ ActionCable.server.restart
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionCable
4
+ # Returns the version of the currently loaded Action Cable as a <tt>Gem::Version</tt>.
5
+ def self.gem_version
6
+ Gem::Version.new VERSION::STRING
7
+ end
8
+
9
+ module VERSION
10
+ MAJOR = 6
11
+ MINOR = 0
12
+ TINY = 0
13
+ PRE = nil
14
+
15
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
+ end
17
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionCable
4
+ module Helpers
5
+ module ActionCableHelper
6
+ # Returns an "action-cable-url" meta tag with the value of the URL specified in your
7
+ # configuration. Ensure this is above your JavaScript tag:
8
+ #
9
+ # <head>
10
+ # <%= action_cable_meta_tag %>
11
+ # <%= javascript_include_tag 'application', 'data-turbolinks-track' => 'reload' %>
12
+ # </head>
13
+ #
14
+ # This is then used by Action Cable to determine the URL of your WebSocket server.
15
+ # Your CoffeeScript can then connect to the server without needing to specify the
16
+ # URL directly:
17
+ #
18
+ # #= require cable
19
+ # @App = {}
20
+ # App.cable = Cable.createConsumer()
21
+ #
22
+ # Make sure to specify the correct server location in each of your environment
23
+ # config files:
24
+ #
25
+ # config.action_cable.mount_path = "/cable123"
26
+ # <%= action_cable_meta_tag %> would render:
27
+ # => <meta name="action-cable-url" content="/cable123" />
28
+ #
29
+ # config.action_cable.url = "ws://actioncable.com"
30
+ # <%= action_cable_meta_tag %> would render:
31
+ # => <meta name="action-cable-url" content="ws://actioncable.com" />
32
+ #
33
+ def action_cable_meta_tag
34
+ tag "meta", name: "action-cable-url", content: (
35
+ ActionCable.server.config.url ||
36
+ ActionCable.server.config.mount_path ||
37
+ raise("No Action Cable URL configured -- please configure this at config.action_cable.url")
38
+ )
39
+ end
40
+ end
41
+ end
42
+ end