actioncable 6.0.0

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 +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