actioncable 5.2.4.4 → 6.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -56
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +3 -546
  5. data/app/assets/javascripts/action_cable.js +517 -0
  6. data/lib/action_cable.rb +15 -7
  7. data/lib/action_cable/channel.rb +1 -0
  8. data/lib/action_cable/channel/base.rb +10 -4
  9. data/lib/action_cable/channel/broadcasting.rb +18 -8
  10. data/lib/action_cable/channel/naming.rb +1 -1
  11. data/lib/action_cable/channel/streams.rb +29 -3
  12. data/lib/action_cable/channel/test_case.rb +310 -0
  13. data/lib/action_cable/connection.rb +1 -0
  14. data/lib/action_cable/connection/authorization.rb +1 -1
  15. data/lib/action_cable/connection/base.rb +13 -7
  16. data/lib/action_cable/connection/message_buffer.rb +1 -4
  17. data/lib/action_cable/connection/stream.rb +4 -2
  18. data/lib/action_cable/connection/subscriptions.rb +2 -5
  19. data/lib/action_cable/connection/test_case.rb +234 -0
  20. data/lib/action_cable/connection/web_socket.rb +1 -3
  21. data/lib/action_cable/engine.rb +1 -1
  22. data/lib/action_cable/gem_version.rb +4 -4
  23. data/lib/action_cable/helpers/action_cable_helper.rb +3 -3
  24. data/lib/action_cable/server.rb +0 -1
  25. data/lib/action_cable/server/base.rb +9 -4
  26. data/lib/action_cable/server/broadcasting.rb +1 -1
  27. data/lib/action_cable/server/worker.rb +6 -8
  28. data/lib/action_cable/subscription_adapter.rb +1 -0
  29. data/lib/action_cable/subscription_adapter/base.rb +4 -0
  30. data/lib/action_cable/subscription_adapter/postgresql.rb +28 -9
  31. data/lib/action_cable/subscription_adapter/redis.rb +4 -2
  32. data/lib/action_cable/subscription_adapter/test.rb +40 -0
  33. data/lib/action_cable/test_case.rb +11 -0
  34. data/lib/action_cable/test_helper.rb +133 -0
  35. data/lib/rails/generators/channel/USAGE +5 -6
  36. data/lib/rails/generators/channel/channel_generator.rb +6 -3
  37. data/lib/rails/generators/channel/templates/{assets → javascript}/channel.js.tt +6 -4
  38. data/lib/rails/generators/channel/templates/javascript/consumer.js.tt +6 -0
  39. data/lib/rails/generators/channel/templates/javascript/index.js.tt +5 -0
  40. data/lib/rails/generators/test_unit/channel_generator.rb +20 -0
  41. data/lib/rails/generators/test_unit/templates/channel_test.rb.tt +8 -0
  42. metadata +40 -16
  43. data/lib/assets/compiled/action_cable.js +0 -601
  44. data/lib/rails/generators/channel/templates/assets/cable.js.tt +0 -13
  45. data/lib/rails/generators/channel/templates/assets/channel.coffee.tt +0 -14
@@ -40,7 +40,7 @@ module ActionCable
40
40
  end
41
41
 
42
42
  def broadcast(message)
43
- server.logger.debug "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}"
43
+ server.logger.debug { "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}" }
44
44
 
45
45
  payload = { broadcasting: broadcasting, message: message, coder: coder }
46
46
  ActiveSupport::Notifications.instrument("broadcast.action_cable", payload) do
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "active_support/callbacks"
4
4
  require "active_support/core_ext/module/attribute_accessors_per_thread"
5
+ require "action_cable/server/worker/active_record_connection_management"
5
6
  require "concurrent"
6
7
 
7
8
  module ActionCable
@@ -56,19 +57,16 @@ module ActionCable
56
57
 
57
58
  def invoke(receiver, method, *args, connection:, &block)
58
59
  work(connection) do
59
- begin
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")
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")
64
64
 
65
- receiver.handle_exception if receiver.respond_to?(:handle_exception)
66
- end
65
+ receiver.handle_exception if receiver.respond_to?(:handle_exception)
67
66
  end
68
67
  end
69
68
 
70
69
  private
71
-
72
70
  def logger
73
71
  ActionCable.server.logger
74
72
  end
@@ -5,6 +5,7 @@ module ActionCable
5
5
  extend ActiveSupport::Autoload
6
6
 
7
7
  autoload :Base
8
+ autoload :Test
8
9
  autoload :SubscriberMap
9
10
  autoload :ChannelPrefix
10
11
  end
@@ -25,6 +25,10 @@ module ActionCable
25
25
  def shutdown
26
26
  raise NotImplementedError
27
27
  end
28
+
29
+ def identifier
30
+ @server.config.cable[:id] ||= "ActionCable-PID-#{$$}"
31
+ end
28
32
  end
29
33
  end
30
34
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- gem "pg", ">= 0.18", "< 2.0"
3
+ gem "pg", "~> 1.1"
4
4
  require "pg"
5
5
  require "thread"
6
6
  require "digest/sha1"
@@ -8,13 +8,15 @@ require "digest/sha1"
8
8
  module ActionCable
9
9
  module SubscriptionAdapter
10
10
  class PostgreSQL < Base # :nodoc:
11
+ prepend ChannelPrefix
12
+
11
13
  def initialize(*)
12
14
  super
13
15
  @listener = nil
14
16
  end
15
17
 
16
18
  def broadcast(channel, payload)
17
- with_connection do |pg_conn|
19
+ with_broadcast_connection do |pg_conn|
18
20
  pg_conn.exec("NOTIFY #{pg_conn.escape_identifier(channel_identifier(channel))}, '#{pg_conn.escape_string(payload)}'")
19
21
  end
20
22
  end
@@ -31,14 +33,25 @@ module ActionCable
31
33
  listener.shutdown
32
34
  end
33
35
 
34
- def with_connection(&block) # :nodoc:
35
- ActiveRecord::Base.connection_pool.with_connection do |ar_conn|
36
- 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
37
43
 
38
- unless pg_conn.is_a?(PG::Connection)
39
- raise "The Active Record database must be PostgreSQL in order to use the PostgreSQL Action Cable storage adapter"
40
- 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
41
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)
42
55
  yield pg_conn
43
56
  end
44
57
  end
@@ -52,6 +65,12 @@ module ActionCable
52
65
  @listener || @server.mutex.synchronize { @listener ||= Listener.new(self, @server.event_loop) }
53
66
  end
54
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
+
55
74
  class Listener < SubscriberMap
56
75
  def initialize(adapter, event_loop)
57
76
  super()
@@ -67,7 +86,7 @@ module ActionCable
67
86
  end
68
87
 
69
88
  def listen
70
- @adapter.with_connection do |pg_conn|
89
+ @adapter.with_subscriptions_connection do |pg_conn|
71
90
  catch :shutdown do
72
91
  loop do
73
92
  until @queue.empty?
@@ -5,6 +5,8 @@ require "thread"
5
5
  gem "redis", ">= 3", "< 5"
6
6
  require "redis"
7
7
 
8
+ require "active_support/core_ext/hash/except"
9
+
8
10
  module ActionCable
9
11
  module SubscriptionAdapter
10
12
  class Redis < Base # :nodoc:
@@ -13,7 +15,7 @@ module ActionCable
13
15
  # Overwrite this factory method for Redis connections if you want to use a different Redis library than the redis gem.
14
16
  # This is needed, for example, when using Makara proxies for distributed Redis.
15
17
  cattr_accessor :redis_connector, default: ->(config) do
16
- ::Redis.new(config.slice(:url, :host, :port, :db, :password))
18
+ ::Redis.new(config.except(:adapter, :channel_prefix))
17
19
  end
18
20
 
19
21
  def initialize(*)
@@ -54,7 +56,7 @@ module ActionCable
54
56
  end
55
57
 
56
58
  def redis_connection
57
- self.class.redis_connector.call(@server.config.cable)
59
+ self.class.redis_connector.call(@server.config.cable.merge(id: identifier))
58
60
  end
59
61
 
60
62
  class Listener < SubscriberMap
@@ -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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/test_case"
4
+
5
+ module ActionCable
6
+ class TestCase < ActiveSupport::TestCase
7
+ include ActionCable::TestHelper
8
+
9
+ ActiveSupport.run_load_hooks(:action_cable_test_case, self)
10
+ end
11
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionCable
4
+ # Provides helper methods for testing Action Cable broadcasting
5
+ module TestHelper
6
+ def before_setup # :nodoc:
7
+ server = ActionCable.server
8
+ test_adapter = ActionCable::SubscriptionAdapter::Test.new(server)
9
+
10
+ @old_pubsub_adapter = server.pubsub
11
+
12
+ server.instance_variable_set(:@pubsub, test_adapter)
13
+ super
14
+ end
15
+
16
+ def after_teardown # :nodoc:
17
+ super
18
+ ActionCable.server.instance_variable_set(:@pubsub, @old_pubsub_adapter)
19
+ end
20
+
21
+ # Asserts that the number of broadcasted messages to the stream matches the given number.
22
+ #
23
+ # def test_broadcasts
24
+ # assert_broadcasts 'messages', 0
25
+ # ActionCable.server.broadcast 'messages', { text: 'hello' }
26
+ # assert_broadcasts 'messages', 1
27
+ # ActionCable.server.broadcast 'messages', { text: 'world' }
28
+ # assert_broadcasts 'messages', 2
29
+ # end
30
+ #
31
+ # If a block is passed, that block should cause the specified number of
32
+ # messages to be broadcasted.
33
+ #
34
+ # def test_broadcasts_again
35
+ # assert_broadcasts('messages', 1) do
36
+ # ActionCable.server.broadcast 'messages', { text: 'hello' }
37
+ # end
38
+ #
39
+ # assert_broadcasts('messages', 2) do
40
+ # ActionCable.server.broadcast 'messages', { text: 'hi' }
41
+ # ActionCable.server.broadcast 'messages', { text: 'how are you?' }
42
+ # end
43
+ # end
44
+ #
45
+ def assert_broadcasts(stream, number, &block)
46
+ if block_given?
47
+ original_count = broadcasts_size(stream)
48
+ assert_nothing_raised(&block)
49
+ new_count = broadcasts_size(stream)
50
+ actual_count = new_count - original_count
51
+ else
52
+ actual_count = broadcasts_size(stream)
53
+ end
54
+
55
+ assert_equal number, actual_count, "#{number} broadcasts to #{stream} expected, but #{actual_count} were sent"
56
+ end
57
+
58
+ # Asserts that no messages have been sent to the stream.
59
+ #
60
+ # def test_no_broadcasts
61
+ # assert_no_broadcasts 'messages'
62
+ # ActionCable.server.broadcast 'messages', { text: 'hi' }
63
+ # assert_broadcasts 'messages', 1
64
+ # end
65
+ #
66
+ # If a block is passed, that block should not cause any message to be sent.
67
+ #
68
+ # def test_broadcasts_again
69
+ # assert_no_broadcasts 'messages' do
70
+ # # No job messages should be sent from this block
71
+ # end
72
+ # end
73
+ #
74
+ # Note: This assertion is simply a shortcut for:
75
+ #
76
+ # assert_broadcasts 'messages', 0, &block
77
+ #
78
+ def assert_no_broadcasts(stream, &block)
79
+ assert_broadcasts stream, 0, &block
80
+ end
81
+
82
+ # Asserts that the specified message has been sent to the stream.
83
+ #
84
+ # def test_assert_transmitted_message
85
+ # ActionCable.server.broadcast 'messages', text: 'hello'
86
+ # assert_broadcast_on('messages', text: 'hello')
87
+ # end
88
+ #
89
+ # If a block is passed, that block should cause a message with the specified data to be sent.
90
+ #
91
+ # def test_assert_broadcast_on_again
92
+ # assert_broadcast_on('messages', text: 'hello') do
93
+ # ActionCable.server.broadcast 'messages', text: 'hello'
94
+ # end
95
+ # end
96
+ #
97
+ def assert_broadcast_on(stream, data, &block)
98
+ # Encode to JSON and back–we want to use this value to compare
99
+ # with decoded JSON.
100
+ # Comparing JSON strings doesn't work due to the order if the keys.
101
+ serialized_msg =
102
+ ActiveSupport::JSON.decode(ActiveSupport::JSON.encode(data))
103
+
104
+ new_messages = broadcasts(stream)
105
+ if block_given?
106
+ old_messages = new_messages
107
+ clear_messages(stream)
108
+
109
+ assert_nothing_raised(&block)
110
+ new_messages = broadcasts(stream)
111
+ clear_messages(stream)
112
+
113
+ # Restore all sent messages
114
+ (old_messages + new_messages).each { |m| pubsub_adapter.broadcast(stream, m) }
115
+ end
116
+
117
+ message = new_messages.find { |msg| ActiveSupport::JSON.decode(msg) == serialized_msg }
118
+
119
+ assert message, "No messages sent with #{data} to #{stream}"
120
+ end
121
+
122
+ def pubsub_adapter # :nodoc:
123
+ ActionCable.server.pubsub
124
+ end
125
+
126
+ delegate :broadcasts, :clear_messages, to: :pubsub_adapter
127
+
128
+ private
129
+ def broadcasts_size(channel)
130
+ broadcasts(channel).size
131
+ end
132
+ end
133
+ end
@@ -1,14 +1,13 @@
1
1
  Description:
2
2
  ============
3
- Stubs out a new cable channel for the server (in Ruby) and client (in CoffeeScript).
3
+ Generates a new cable channel for the server (in Ruby) and client (in JavaScript).
4
4
  Pass the channel name, either CamelCased or under_scored, and an optional list of channel actions as arguments.
5
5
 
6
- Note: Turn on the cable connection in app/assets/javascripts/cable.js after generating any channels.
7
-
8
6
  Example:
9
7
  ========
10
- rails generate channel Chat speak
8
+ bin/rails generate channel Chat speak
11
9
 
12
- creates a Chat channel class and CoffeeScript asset:
10
+ creates a Chat channel class, test and JavaScript asset:
13
11
  Channel: app/channels/chat_channel.rb
14
- Assets: app/assets/javascripts/channels/chat.coffee
12
+ Test: test/channels/chat_channel_test.rb
13
+ Assets: app/javascript/channels/chat_channel.js
@@ -11,15 +11,18 @@ module Rails
11
11
 
12
12
  check_class_collision suffix: "Channel"
13
13
 
14
+ hook_for :test_framework
15
+
14
16
  def create_channel_file
15
17
  template "channel.rb", File.join("app/channels", class_path, "#{file_name}_channel.rb")
16
18
 
17
19
  if options[:assets]
18
20
  if behavior == :invoke
19
- template "assets/cable.js", "app/assets/javascripts/cable.js"
21
+ template "javascript/index.js", "app/javascript/channels/index.js"
22
+ template "javascript/consumer.js", "app/javascript/channels/consumer.js"
20
23
  end
21
24
 
22
- js_template "assets/channel", File.join("app/assets/javascripts/channels", class_path, "#{file_name}")
25
+ js_template "javascript/channel", File.join("app/javascript/channels", class_path, "#{file_name}_channel")
23
26
  end
24
27
 
25
28
  generate_application_cable_files
@@ -27,7 +30,7 @@ module Rails
27
30
 
28
31
  private
29
32
  def file_name
30
- @_file_name ||= super.gsub(/_channel/i, "")
33
+ @_file_name ||= super.sub(/_channel\z/i, "")
31
34
  end
32
35
 
33
36
  # FIXME: Change these files to symlinks once RubyGems 2.5.0 is required.
@@ -1,13 +1,15 @@
1
- App.<%= class_name.underscore %> = App.cable.subscriptions.create("<%= class_name %>Channel", {
2
- connected: function() {
1
+ import consumer from "./consumer"
2
+
3
+ consumer.subscriptions.create("<%= class_name %>Channel", {
4
+ connected() {
3
5
  // Called when the subscription is ready for use on the server
4
6
  },
5
7
 
6
- disconnected: function() {
8
+ disconnected() {
7
9
  // Called when the subscription has been terminated by the server
8
10
  },
9
11
 
10
- received: function(data) {
12
+ received(data) {
11
13
  // Called when there's incoming data on the websocket for this channel
12
14
  }<%= actions.any? ? ",\n" : '' %>
13
15
  <% actions.each do |action| -%>
@@ -0,0 +1,6 @@
1
+ // Action Cable provides the framework to deal with WebSockets in Rails.
2
+ // You can generate new channels where WebSocket features live using the `bin/rails generate channel` command.
3
+
4
+ import { createConsumer } from "@rails/actioncable"
5
+
6
+ export default createConsumer()
@@ -0,0 +1,5 @@
1
+ // Load all the channels within this directory and all subdirectories.
2
+ // Channel files must be named *_channel.js.
3
+
4
+ const channels = require.context('.', true, /_channel\.js$/)
5
+ channels.keys().forEach(channels)
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestUnit
4
+ module Generators
5
+ class ChannelGenerator < ::Rails::Generators::NamedBase
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ check_class_collision suffix: "ChannelTest"
9
+
10
+ def create_test_files
11
+ template "channel_test.rb", File.join("test/channels", class_path, "#{file_name}_channel_test.rb")
12
+ end
13
+
14
+ private
15
+ def file_name # :doc:
16
+ @_file_name ||= super.sub(/_channel\z/i, "")
17
+ end
18
+ end
19
+ end
20
+ end