actioncable 5.0.1 → 6.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +31 -117
- data/MIT-LICENSE +1 -1
- data/README.md +4 -535
- data/app/assets/javascripts/action_cable.js +517 -0
- data/lib/action_cable.rb +20 -10
- data/lib/action_cable/channel.rb +3 -0
- data/lib/action_cable/channel/base.rb +31 -23
- data/lib/action_cable/channel/broadcasting.rb +22 -10
- data/lib/action_cable/channel/callbacks.rb +4 -2
- data/lib/action_cable/channel/naming.rb +5 -2
- data/lib/action_cable/channel/periodic_timers.rb +4 -3
- data/lib/action_cable/channel/streams.rb +39 -11
- data/lib/action_cable/channel/test_case.rb +310 -0
- data/lib/action_cable/connection.rb +3 -2
- data/lib/action_cable/connection/authorization.rb +8 -6
- data/lib/action_cable/connection/base.rb +34 -26
- data/lib/action_cable/connection/client_socket.rb +20 -18
- data/lib/action_cable/connection/identification.rb +5 -4
- data/lib/action_cable/connection/internal_channel.rb +4 -2
- data/lib/action_cable/connection/message_buffer.rb +3 -2
- data/lib/action_cable/connection/stream.rb +9 -5
- data/lib/action_cable/connection/stream_event_loop.rb +4 -2
- data/lib/action_cable/connection/subscriptions.rb +14 -13
- data/lib/action_cable/connection/tagged_logger_proxy.rb +4 -2
- data/lib/action_cable/connection/test_case.rb +234 -0
- data/lib/action_cable/connection/web_socket.rb +7 -5
- data/lib/action_cable/engine.rb +7 -5
- data/lib/action_cable/gem_version.rb +5 -3
- data/lib/action_cable/helpers/action_cable_helper.rb +6 -4
- data/lib/action_cable/remote_connections.rb +9 -4
- data/lib/action_cable/server.rb +2 -1
- data/lib/action_cable/server/base.rb +17 -10
- data/lib/action_cable/server/broadcasting.rb +9 -3
- data/lib/action_cable/server/configuration.rb +21 -22
- data/lib/action_cable/server/connections.rb +2 -0
- data/lib/action_cable/server/worker.rb +11 -11
- data/lib/action_cable/server/worker/active_record_connection_management.rb +2 -0
- data/lib/action_cable/subscription_adapter.rb +4 -0
- data/lib/action_cable/subscription_adapter/async.rb +3 -1
- data/lib/action_cable/subscription_adapter/base.rb +6 -0
- data/lib/action_cable/subscription_adapter/channel_prefix.rb +28 -0
- data/lib/action_cable/subscription_adapter/inline.rb +2 -0
- data/lib/action_cable/subscription_adapter/postgresql.rb +40 -14
- data/lib/action_cable/subscription_adapter/redis.rb +19 -11
- data/lib/action_cable/subscription_adapter/subscriber_map.rb +3 -1
- data/lib/action_cable/subscription_adapter/test.rb +40 -0
- data/lib/action_cable/test_case.rb +11 -0
- data/lib/action_cable/test_helper.rb +133 -0
- data/lib/action_cable/version.rb +3 -1
- data/lib/rails/generators/channel/USAGE +5 -6
- data/lib/rails/generators/channel/channel_generator.rb +16 -11
- data/lib/rails/generators/channel/templates/application_cable/{channel.rb → channel.rb.tt} +0 -0
- data/lib/rails/generators/channel/templates/application_cable/{connection.rb → connection.rb.tt} +0 -0
- data/lib/rails/generators/channel/templates/{channel.rb → channel.rb.tt} +0 -0
- data/lib/rails/generators/channel/templates/{assets/channel.js → javascript/channel.js.tt} +6 -4
- data/lib/rails/generators/channel/templates/javascript/consumer.js.tt +6 -0
- data/lib/rails/generators/channel/templates/javascript/index.js.tt +5 -0
- data/lib/rails/generators/test_unit/channel_generator.rb +20 -0
- data/lib/rails/generators/test_unit/templates/channel_test.rb.tt +8 -0
- metadata +46 -38
- data/lib/action_cable/connection/faye_client_socket.rb +0 -48
- data/lib/action_cable/connection/faye_event_loop.rb +0 -44
- data/lib/action_cable/subscription_adapter/evented_redis.rb +0 -79
- data/lib/assets/compiled/action_cable.js +0 -597
- data/lib/rails/generators/channel/templates/assets/cable.js +0 -13
- 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.
|
42
|
-
|
43
|
-
|
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 :
|
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(
|
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
|
-
|
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 =
|
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,6 +1,9 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
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
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
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 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,50 +1,76 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
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
|
-
|
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
|
32
|
-
ActiveRecord::Base.connection_pool.
|
33
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
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.
|
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
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
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
|
-
|
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
|
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(
|
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(
|
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(
|
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(
|
140
|
+
when_connected { send_command("unsubscribe", channel) }
|
133
141
|
end
|
134
142
|
end
|
135
143
|
|
@@ -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
|