actioncable 5.0.1 → 6.1.3

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 (67) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +31 -117
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +4 -535
  5. data/app/assets/javascripts/action_cable.js +517 -0
  6. data/lib/action_cable.rb +20 -10
  7. data/lib/action_cable/channel.rb +3 -0
  8. data/lib/action_cable/channel/base.rb +31 -23
  9. data/lib/action_cable/channel/broadcasting.rb +22 -10
  10. data/lib/action_cable/channel/callbacks.rb +4 -2
  11. data/lib/action_cable/channel/naming.rb +5 -2
  12. data/lib/action_cable/channel/periodic_timers.rb +4 -3
  13. data/lib/action_cable/channel/streams.rb +39 -11
  14. data/lib/action_cable/channel/test_case.rb +310 -0
  15. data/lib/action_cable/connection.rb +3 -2
  16. data/lib/action_cable/connection/authorization.rb +8 -6
  17. data/lib/action_cable/connection/base.rb +34 -26
  18. data/lib/action_cable/connection/client_socket.rb +20 -18
  19. data/lib/action_cable/connection/identification.rb +5 -4
  20. data/lib/action_cable/connection/internal_channel.rb +4 -2
  21. data/lib/action_cable/connection/message_buffer.rb +3 -2
  22. data/lib/action_cable/connection/stream.rb +9 -5
  23. data/lib/action_cable/connection/stream_event_loop.rb +4 -2
  24. data/lib/action_cable/connection/subscriptions.rb +14 -13
  25. data/lib/action_cable/connection/tagged_logger_proxy.rb +4 -2
  26. data/lib/action_cable/connection/test_case.rb +234 -0
  27. data/lib/action_cable/connection/web_socket.rb +7 -5
  28. data/lib/action_cable/engine.rb +7 -5
  29. data/lib/action_cable/gem_version.rb +5 -3
  30. data/lib/action_cable/helpers/action_cable_helper.rb +6 -4
  31. data/lib/action_cable/remote_connections.rb +9 -4
  32. data/lib/action_cable/server.rb +2 -1
  33. data/lib/action_cable/server/base.rb +17 -10
  34. data/lib/action_cable/server/broadcasting.rb +9 -3
  35. data/lib/action_cable/server/configuration.rb +21 -22
  36. data/lib/action_cable/server/connections.rb +2 -0
  37. data/lib/action_cable/server/worker.rb +11 -11
  38. data/lib/action_cable/server/worker/active_record_connection_management.rb +2 -0
  39. data/lib/action_cable/subscription_adapter.rb +4 -0
  40. data/lib/action_cable/subscription_adapter/async.rb +3 -1
  41. data/lib/action_cable/subscription_adapter/base.rb +6 -0
  42. data/lib/action_cable/subscription_adapter/channel_prefix.rb +28 -0
  43. data/lib/action_cable/subscription_adapter/inline.rb +2 -0
  44. data/lib/action_cable/subscription_adapter/postgresql.rb +40 -14
  45. data/lib/action_cable/subscription_adapter/redis.rb +19 -11
  46. data/lib/action_cable/subscription_adapter/subscriber_map.rb +3 -1
  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 +3 -1
  51. data/lib/rails/generators/channel/USAGE +5 -6
  52. data/lib/rails/generators/channel/channel_generator.rb +16 -11
  53. data/lib/rails/generators/channel/templates/application_cable/{channel.rb → channel.rb.tt} +0 -0
  54. data/lib/rails/generators/channel/templates/application_cable/{connection.rb → connection.rb.tt} +0 -0
  55. data/lib/rails/generators/channel/templates/{channel.rb → channel.rb.tt} +0 -0
  56. data/lib/rails/generators/channel/templates/{assets/channel.js → javascript/channel.js.tt} +6 -4
  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 +46 -38
  62. data/lib/action_cable/connection/faye_client_socket.rb +0 -48
  63. data/lib/action_cable/connection/faye_event_loop.rb +0 -44
  64. data/lib/action_cable/subscription_adapter/evented_redis.rb +0 -79
  65. data/lib/assets/compiled/action_cable.js +0 -597
  66. data/lib/rails/generators/channel/templates/assets/cable.js +0 -13
  67. data/lib/rails/generators/channel/templates/assets/channel.coffee +0 -14
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionCable
2
4
  module Server
3
5
  # Broadcasting is how other parts of your application can send messages to a channel's subscribers. As explained in Channel, most of the time, these
@@ -38,9 +40,13 @@ module ActionCable
38
40
  end
39
41
 
40
42
  def broadcast(message)
41
- server.logger.info "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}"
42
- encoded = coder ? coder.encode(message) : message
43
- server.pubsub.broadcast broadcasting, encoded
43
+ server.logger.debug { "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}" }
44
+
45
+ payload = { broadcasting: broadcasting, message: message, coder: coder }
46
+ ActiveSupport::Notifications.instrument("broadcast.action_cable", payload) do
47
+ encoded = coder ? coder.encode(message) : message
48
+ server.pubsub.broadcast broadcasting, encoded
49
+ end
44
50
  end
45
51
  end
46
52
  end
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionCable
2
4
  module Server
3
5
  # An instance of this configuration object is available via ActionCable.server.config, which allows you to tweak Action Cable configuration
4
6
  # in a Rails config initializer.
5
7
  class Configuration
6
8
  attr_accessor :logger, :log_tags
7
- attr_accessor :use_faye, :connection_class, :worker_pool_size
9
+ attr_accessor :connection_class, :worker_pool_size
8
10
  attr_accessor :disable_request_forgery_protection, :allowed_request_origins, :allow_same_origin_as_host
9
11
  attr_accessor :cable, :url, :mount_path
10
12
 
@@ -22,36 +24,33 @@ module ActionCable
22
24
  # If the adapter cannot be found, this will default to the Redis adapter.
23
25
  # Also makes sure proper dependencies are required.
24
26
  def pubsub_adapter
25
- adapter = (cable.fetch('adapter') { 'redis' })
27
+ adapter = (cable.fetch("adapter") { "redis" })
28
+
29
+ # Require the adapter itself and give useful feedback about
30
+ # 1. Missing adapter gems and
31
+ # 2. Adapter gems' missing dependencies.
26
32
  path_to_adapter = "action_cable/subscription_adapter/#{adapter}"
27
33
  begin
28
34
  require path_to_adapter
29
- rescue Gem::LoadError => e
30
- raise Gem::LoadError, "Specified '#{adapter}' for Action Cable pubsub adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by Action Cable)."
31
35
  rescue LoadError => e
32
- raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/cable.yml is valid. If you use an adapter other than 'postgresql' or 'redis' add the necessary adapter gem to the Gemfile.", e.backtrace
36
+ # We couldn't require the adapter itself. Raise an exception that
37
+ # points out config typos and missing gems.
38
+ if e.path == path_to_adapter
39
+ # We can assume that a non-builtin adapter was specified, so it's
40
+ # either misspelled or missing from Gemfile.
41
+ raise e.class, "Could not load the '#{adapter}' Action Cable pubsub adapter. Ensure that the adapter is spelled correctly in config/cable.yml and that you've added the necessary adapter gem to your Gemfile.", e.backtrace
42
+
43
+ # Bubbled up from the adapter require. Prefix the exception message
44
+ # with some guidance about how to address it and reraise.
45
+ else
46
+ raise e.class, "Error loading the '#{adapter}' Action Cable pubsub adapter. Missing a gem it depends on? #{e.message}", e.backtrace
47
+ end
33
48
  end
34
49
 
35
50
  adapter = adapter.camelize
36
- adapter = 'PostgreSQL' if adapter == 'Postgresql'
51
+ adapter = "PostgreSQL" if adapter == "Postgresql"
37
52
  "ActionCable::SubscriptionAdapter::#{adapter}".constantize
38
53
  end
39
-
40
- def event_loop_class
41
- if use_faye
42
- ActionCable::Connection::FayeEventLoop
43
- else
44
- ActionCable::Connection::StreamEventLoop
45
- end
46
- end
47
-
48
- def client_socket_class
49
- if use_faye
50
- ActionCable::Connection::FayeClientSocket
51
- else
52
- ActionCable::Connection::ClientSocket
53
- end
54
- end
55
54
  end
56
55
  end
57
56
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionCable
2
4
  module Server
3
5
  # Collection class for all the connections that have been established on this specific server. Remember, usually you'll run many Action Cable servers, so
@@ -1,6 +1,9 @@
1
- require 'active_support/callbacks'
2
- require 'active_support/core_ext/module/attribute_accessors_per_thread'
3
- require 'concurrent'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/callbacks"
4
+ require "active_support/core_ext/module/attribute_accessors_per_thread"
5
+ require "action_cable/server/worker/active_record_connection_management"
6
+ require "concurrent"
4
7
 
5
8
  module ActionCable
6
9
  module Server
@@ -54,19 +57,16 @@ module ActionCable
54
57
 
55
58
  def invoke(receiver, method, *args, connection:, &block)
56
59
  work(connection) do
57
- begin
58
- receiver.send method, *args, &block
59
- rescue Exception => e
60
- logger.error "There was an exception - #{e.class}(#{e.message})"
61
- logger.error e.backtrace.join("\n")
60
+ receiver.send method, *args, &block
61
+ rescue Exception => e
62
+ logger.error "There was an exception - #{e.class}(#{e.message})"
63
+ logger.error e.backtrace.join("\n")
62
64
 
63
- receiver.handle_exception if receiver.respond_to?(:handle_exception)
64
- end
65
+ receiver.handle_exception if receiver.respond_to?(:handle_exception)
65
66
  end
66
67
  end
67
68
 
68
69
  private
69
-
70
70
  def logger
71
71
  ActionCable.server.logger
72
72
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionCable
2
4
  module Server
3
5
  class Worker
@@ -1,8 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionCable
2
4
  module SubscriptionAdapter
3
5
  extend ActiveSupport::Autoload
4
6
 
5
7
  autoload :Base
8
+ autoload :Test
6
9
  autoload :SubscriberMap
10
+ autoload :ChannelPrefix
7
11
  end
8
12
  end
@@ -1,4 +1,6 @@
1
- require 'action_cable/subscription_adapter/inline'
1
+ # frozen_string_literal: true
2
+
3
+ require "action_cable/subscription_adapter/inline"
2
4
 
3
5
  module ActionCable
4
6
  module SubscriptionAdapter
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionCable
2
4
  module SubscriptionAdapter
3
5
  class Base
@@ -23,6 +25,10 @@ module ActionCable
23
25
  def shutdown
24
26
  raise NotImplementedError
25
27
  end
28
+
29
+ def identifier
30
+ @server.config.cable[:id] ||= "ActionCable-PID-#{$$}"
31
+ end
26
32
  end
27
33
  end
28
34
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionCable
4
+ module SubscriptionAdapter
5
+ module ChannelPrefix # :nodoc:
6
+ def broadcast(channel, payload)
7
+ channel = channel_with_prefix(channel)
8
+ super
9
+ end
10
+
11
+ def subscribe(channel, callback, success_callback = nil)
12
+ channel = channel_with_prefix(channel)
13
+ super
14
+ end
15
+
16
+ def unsubscribe(channel, callback)
17
+ channel = channel_with_prefix(channel)
18
+ super
19
+ end
20
+
21
+ private
22
+ # Returns the channel name, including channel_prefix specified in cable.yml
23
+ def channel_with_prefix(channel)
24
+ [@server.config.cable[:channel_prefix], channel].compact.join(":")
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionCable
2
4
  module SubscriptionAdapter
3
5
  class Inline < Base # :nodoc:
@@ -1,50 +1,76 @@
1
- gem 'pg', '~> 0.18'
2
- require 'pg'
3
- require 'thread'
1
+ # frozen_string_literal: true
2
+
3
+ gem "pg", "~> 1.1"
4
+ require "pg"
5
+ require "thread"
6
+ require "digest/sha1"
4
7
 
5
8
  module ActionCable
6
9
  module SubscriptionAdapter
7
10
  class PostgreSQL < Base # :nodoc:
11
+ prepend ChannelPrefix
12
+
8
13
  def initialize(*)
9
14
  super
10
15
  @listener = nil
11
16
  end
12
17
 
13
18
  def broadcast(channel, payload)
14
- with_connection do |pg_conn|
15
- pg_conn.exec("NOTIFY #{pg_conn.escape_identifier(channel)}, '#{pg_conn.escape_string(payload)}'")
19
+ with_broadcast_connection do |pg_conn|
20
+ pg_conn.exec("NOTIFY #{pg_conn.escape_identifier(channel_identifier(channel))}, '#{pg_conn.escape_string(payload)}'")
16
21
  end
17
22
  end
18
23
 
19
24
  def subscribe(channel, callback, success_callback = nil)
20
- listener.add_subscriber(channel, callback, success_callback)
25
+ listener.add_subscriber(channel_identifier(channel), callback, success_callback)
21
26
  end
22
27
 
23
28
  def unsubscribe(channel, callback)
24
- listener.remove_subscriber(channel, callback)
29
+ listener.remove_subscriber(channel_identifier(channel), callback)
25
30
  end
26
31
 
27
32
  def shutdown
28
33
  listener.shutdown
29
34
  end
30
35
 
31
- def with_connection(&block) # :nodoc:
32
- ActiveRecord::Base.connection_pool.with_connection do |ar_conn|
33
- pg_conn = ar_conn.raw_connection
36
+ def with_subscriptions_connection(&block) # :nodoc:
37
+ ar_conn = ActiveRecord::Base.connection_pool.checkout.tap do |conn|
38
+ # Action Cable is taking ownership over this database connection, and
39
+ # will perform the necessary cleanup tasks
40
+ ActiveRecord::Base.connection_pool.remove(conn)
41
+ end
42
+ pg_conn = ar_conn.raw_connection
34
43
 
35
- unless pg_conn.is_a?(PG::Connection)
36
- raise 'ActiveRecord database must be Postgres in order to use the Postgres ActionCable storage adapter'
37
- end
44
+ verify!(pg_conn)
45
+ pg_conn.exec("SET application_name = #{pg_conn.escape_identifier(identifier)}")
46
+ yield pg_conn
47
+ ensure
48
+ ar_conn.disconnect!
49
+ end
38
50
 
51
+ def with_broadcast_connection(&block) # :nodoc:
52
+ ActiveRecord::Base.connection_pool.with_connection do |ar_conn|
53
+ pg_conn = ar_conn.raw_connection
54
+ verify!(pg_conn)
39
55
  yield pg_conn
40
56
  end
41
57
  end
42
58
 
43
59
  private
60
+ def channel_identifier(channel)
61
+ channel.size > 63 ? Digest::SHA1.hexdigest(channel) : channel
62
+ end
63
+
44
64
  def listener
45
65
  @listener || @server.mutex.synchronize { @listener ||= Listener.new(self, @server.event_loop) }
46
66
  end
47
67
 
68
+ def verify!(pg_conn)
69
+ unless pg_conn.is_a?(PG::Connection)
70
+ raise "The Active Record database must be PostgreSQL in order to use the PostgreSQL Action Cable storage adapter"
71
+ end
72
+ end
73
+
48
74
  class Listener < SubscriberMap
49
75
  def initialize(adapter, event_loop)
50
76
  super()
@@ -60,7 +86,7 @@ module ActionCable
60
86
  end
61
87
 
62
88
  def listen
63
- @adapter.with_connection do |pg_conn|
89
+ @adapter.with_subscriptions_connection do |pg_conn|
64
90
  catch :shutdown do
65
91
  loop do
66
92
  until @queue.empty?
@@ -1,14 +1,22 @@
1
- require 'thread'
1
+ # frozen_string_literal: true
2
2
 
3
- gem 'redis', '~> 3.0'
4
- require 'redis'
3
+ require "thread"
4
+
5
+ gem "redis", ">= 3", "< 5"
6
+ require "redis"
7
+
8
+ require "active_support/core_ext/hash/except"
5
9
 
6
10
  module ActionCable
7
11
  module SubscriptionAdapter
8
12
  class Redis < Base # :nodoc:
9
- # Overwrite this factory method for redis connections if you want to use a different Redis library than Redis.
13
+ prepend ChannelPrefix
14
+
15
+ # Overwrite this factory method for Redis connections if you want to use a different Redis library than the redis gem.
10
16
  # This is needed, for example, when using Makara proxies for distributed Redis.
11
- cattr_accessor(:redis_connector) { ->(config) { ::Redis.new(url: config[:url]) } }
17
+ cattr_accessor :redis_connector, default: ->(config) do
18
+ ::Redis.new(config.except(:adapter, :channel_prefix))
19
+ end
12
20
 
13
21
  def initialize(*)
14
22
  super
@@ -48,7 +56,7 @@ module ActionCable
48
56
  end
49
57
 
50
58
  def redis_connection
51
- self.class.redis_connector.call(@server.config.cable)
59
+ self.class.redis_connector.call(@server.config.cable.merge(id: identifier))
52
60
  end
53
61
 
54
62
  class Listener < SubscriberMap
@@ -70,9 +78,9 @@ module ActionCable
70
78
 
71
79
  def listen(conn)
72
80
  conn.without_reconnect do
73
- original_client = conn.client
81
+ original_client = conn.respond_to?(:_client) ? conn._client : conn.client
74
82
 
75
- conn.subscribe('_action_cable_internal') do |on|
83
+ conn.subscribe("_action_cable_internal") do |on|
76
84
  on.subscribe do |chan, count|
77
85
  @subscription_lock.synchronize do
78
86
  if count == 1
@@ -111,7 +119,7 @@ module ActionCable
111
119
  return if @thread.nil?
112
120
 
113
121
  when_connected do
114
- send_command('unsubscribe')
122
+ send_command("unsubscribe")
115
123
  @raw_client = nil
116
124
  end
117
125
  end
@@ -123,13 +131,13 @@ module ActionCable
123
131
  @subscription_lock.synchronize do
124
132
  ensure_listener_running
125
133
  @subscribe_callbacks[channel] << on_success
126
- when_connected { send_command('subscribe', channel) }
134
+ when_connected { send_command("subscribe", channel) }
127
135
  end
128
136
  end
129
137
 
130
138
  def remove_channel(channel)
131
139
  @subscription_lock.synchronize do
132
- when_connected { send_command('unsubscribe', channel) }
140
+ when_connected { send_command("unsubscribe", channel) }
133
141
  end
134
142
  end
135
143
 
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionCable
2
4
  module SubscriptionAdapter
3
5
  class SubscriberMap
4
6
  def initialize
5
- @subscribers = Hash.new { |h,k| h[k] = [] }
7
+ @subscribers = Hash.new { |h, k| h[k] = [] }
6
8
  @sync = Mutex.new
7
9
  end
8
10
 
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "async"
4
+
5
+ module ActionCable
6
+ module SubscriptionAdapter
7
+ # == Test adapter for Action Cable
8
+ #
9
+ # The test adapter should be used only in testing. Along with
10
+ # <tt>ActionCable::TestHelper</tt> it makes a great tool to test your Rails application.
11
+ #
12
+ # To use the test adapter set +adapter+ value to +test+ in your +config/cable.yml+ file.
13
+ #
14
+ # NOTE: Test adapter extends the <tt>ActionCable::SubscriptionsAdapter::Async</tt> adapter,
15
+ # so it could be used in system tests too.
16
+ class Test < Async
17
+ def broadcast(channel, payload)
18
+ broadcasts(channel) << payload
19
+ super
20
+ end
21
+
22
+ def broadcasts(channel)
23
+ channels_data[channel] ||= []
24
+ end
25
+
26
+ def clear_messages(channel)
27
+ channels_data[channel] = []
28
+ end
29
+
30
+ def clear
31
+ @channels_data = nil
32
+ end
33
+
34
+ private
35
+ def channels_data
36
+ @channels_data ||= {}
37
+ end
38
+ end
39
+ end
40
+ end