actioncable 5.0.1 → 6.1.3

Sign up to get free protection for your applications and to get access to all the features.
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