actioncable 6.0.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +169 -0
- data/MIT-LICENSE +20 -0
- data/README.md +24 -0
- data/app/assets/javascripts/action_cable.js +517 -0
- data/lib/action_cable.rb +62 -0
- data/lib/action_cable/channel.rb +17 -0
- data/lib/action_cable/channel/base.rb +311 -0
- data/lib/action_cable/channel/broadcasting.rb +41 -0
- data/lib/action_cable/channel/callbacks.rb +37 -0
- data/lib/action_cable/channel/naming.rb +25 -0
- data/lib/action_cable/channel/periodic_timers.rb +78 -0
- data/lib/action_cable/channel/streams.rb +176 -0
- data/lib/action_cable/channel/test_case.rb +310 -0
- data/lib/action_cable/connection.rb +22 -0
- data/lib/action_cable/connection/authorization.rb +15 -0
- data/lib/action_cable/connection/base.rb +264 -0
- data/lib/action_cable/connection/client_socket.rb +157 -0
- data/lib/action_cable/connection/identification.rb +47 -0
- data/lib/action_cable/connection/internal_channel.rb +45 -0
- data/lib/action_cable/connection/message_buffer.rb +54 -0
- data/lib/action_cable/connection/stream.rb +117 -0
- data/lib/action_cable/connection/stream_event_loop.rb +136 -0
- data/lib/action_cable/connection/subscriptions.rb +79 -0
- data/lib/action_cable/connection/tagged_logger_proxy.rb +42 -0
- data/lib/action_cable/connection/test_case.rb +234 -0
- data/lib/action_cable/connection/web_socket.rb +41 -0
- data/lib/action_cable/engine.rb +79 -0
- data/lib/action_cable/gem_version.rb +17 -0
- data/lib/action_cable/helpers/action_cable_helper.rb +42 -0
- data/lib/action_cable/remote_connections.rb +71 -0
- data/lib/action_cable/server.rb +17 -0
- data/lib/action_cable/server/base.rb +94 -0
- data/lib/action_cable/server/broadcasting.rb +54 -0
- data/lib/action_cable/server/configuration.rb +56 -0
- data/lib/action_cable/server/connections.rb +36 -0
- data/lib/action_cable/server/worker.rb +75 -0
- data/lib/action_cable/server/worker/active_record_connection_management.rb +21 -0
- data/lib/action_cable/subscription_adapter.rb +12 -0
- data/lib/action_cable/subscription_adapter/async.rb +29 -0
- data/lib/action_cable/subscription_adapter/base.rb +30 -0
- data/lib/action_cable/subscription_adapter/channel_prefix.rb +28 -0
- data/lib/action_cable/subscription_adapter/inline.rb +37 -0
- data/lib/action_cable/subscription_adapter/postgresql.rb +132 -0
- data/lib/action_cable/subscription_adapter/redis.rb +181 -0
- data/lib/action_cable/subscription_adapter/subscriber_map.rb +59 -0
- 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 +10 -0
- data/lib/rails/generators/channel/USAGE +13 -0
- data/lib/rails/generators/channel/channel_generator.rb +52 -0
- data/lib/rails/generators/channel/templates/application_cable/channel.rb.tt +4 -0
- data/lib/rails/generators/channel/templates/application_cable/connection.rb.tt +4 -0
- data/lib/rails/generators/channel/templates/channel.rb.tt +16 -0
- data/lib/rails/generators/channel/templates/javascript/channel.js.tt +20 -0
- 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 +149 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_cable/subscription_adapter/inline"
|
4
|
+
|
5
|
+
module ActionCable
|
6
|
+
module SubscriptionAdapter
|
7
|
+
class Async < Inline # :nodoc:
|
8
|
+
private
|
9
|
+
def new_subscriber_map
|
10
|
+
AsyncSubscriberMap.new(server.event_loop)
|
11
|
+
end
|
12
|
+
|
13
|
+
class AsyncSubscriberMap < SubscriberMap
|
14
|
+
def initialize(event_loop)
|
15
|
+
@event_loop = event_loop
|
16
|
+
super()
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_subscriber(*)
|
20
|
+
@event_loop.post { super }
|
21
|
+
end
|
22
|
+
|
23
|
+
def invoke_callback(*)
|
24
|
+
@event_loop.post { super }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionCable
|
4
|
+
module SubscriptionAdapter
|
5
|
+
class Base
|
6
|
+
attr_reader :logger, :server
|
7
|
+
|
8
|
+
def initialize(server)
|
9
|
+
@server = server
|
10
|
+
@logger = @server.logger
|
11
|
+
end
|
12
|
+
|
13
|
+
def broadcast(channel, payload)
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
|
17
|
+
def subscribe(channel, message_callback, success_callback = nil)
|
18
|
+
raise NotImplementedError
|
19
|
+
end
|
20
|
+
|
21
|
+
def unsubscribe(channel, message_callback)
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
|
25
|
+
def shutdown
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
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
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionCable
|
4
|
+
module SubscriptionAdapter
|
5
|
+
class Inline < Base # :nodoc:
|
6
|
+
def initialize(*)
|
7
|
+
super
|
8
|
+
@subscriber_map = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def broadcast(channel, payload)
|
12
|
+
subscriber_map.broadcast(channel, payload)
|
13
|
+
end
|
14
|
+
|
15
|
+
def subscribe(channel, callback, success_callback = nil)
|
16
|
+
subscriber_map.add_subscriber(channel, callback, success_callback)
|
17
|
+
end
|
18
|
+
|
19
|
+
def unsubscribe(channel, callback)
|
20
|
+
subscriber_map.remove_subscriber(channel, callback)
|
21
|
+
end
|
22
|
+
|
23
|
+
def shutdown
|
24
|
+
# nothing to do
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def subscriber_map
|
29
|
+
@subscriber_map || @server.mutex.synchronize { @subscriber_map ||= new_subscriber_map }
|
30
|
+
end
|
31
|
+
|
32
|
+
def new_subscriber_map
|
33
|
+
SubscriberMap.new
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
gem "pg", ">= 0.18", "< 2.0"
|
4
|
+
require "pg"
|
5
|
+
require "thread"
|
6
|
+
require "digest/sha1"
|
7
|
+
|
8
|
+
module ActionCable
|
9
|
+
module SubscriptionAdapter
|
10
|
+
class PostgreSQL < Base # :nodoc:
|
11
|
+
prepend ChannelPrefix
|
12
|
+
|
13
|
+
def initialize(*)
|
14
|
+
super
|
15
|
+
@listener = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def broadcast(channel, 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)}'")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def subscribe(channel, callback, success_callback = nil)
|
25
|
+
listener.add_subscriber(channel_identifier(channel), callback, success_callback)
|
26
|
+
end
|
27
|
+
|
28
|
+
def unsubscribe(channel, callback)
|
29
|
+
listener.remove_subscriber(channel_identifier(channel), callback)
|
30
|
+
end
|
31
|
+
|
32
|
+
def shutdown
|
33
|
+
listener.shutdown
|
34
|
+
end
|
35
|
+
|
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
|
43
|
+
|
44
|
+
verify!(pg_conn)
|
45
|
+
yield pg_conn
|
46
|
+
ensure
|
47
|
+
ar_conn.disconnect!
|
48
|
+
end
|
49
|
+
|
50
|
+
def with_broadcast_connection(&block) # :nodoc:
|
51
|
+
ActiveRecord::Base.connection_pool.with_connection do |ar_conn|
|
52
|
+
pg_conn = ar_conn.raw_connection
|
53
|
+
verify!(pg_conn)
|
54
|
+
yield pg_conn
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
def channel_identifier(channel)
|
60
|
+
channel.size > 63 ? Digest::SHA1.hexdigest(channel) : channel
|
61
|
+
end
|
62
|
+
|
63
|
+
def listener
|
64
|
+
@listener || @server.mutex.synchronize { @listener ||= Listener.new(self, @server.event_loop) }
|
65
|
+
end
|
66
|
+
|
67
|
+
def verify!(pg_conn)
|
68
|
+
unless pg_conn.is_a?(PG::Connection)
|
69
|
+
raise "The Active Record database must be PostgreSQL in order to use the PostgreSQL Action Cable storage adapter"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class Listener < SubscriberMap
|
74
|
+
def initialize(adapter, event_loop)
|
75
|
+
super()
|
76
|
+
|
77
|
+
@adapter = adapter
|
78
|
+
@event_loop = event_loop
|
79
|
+
@queue = Queue.new
|
80
|
+
|
81
|
+
@thread = Thread.new do
|
82
|
+
Thread.current.abort_on_exception = true
|
83
|
+
listen
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def listen
|
88
|
+
@adapter.with_subscriptions_connection do |pg_conn|
|
89
|
+
catch :shutdown do
|
90
|
+
loop do
|
91
|
+
until @queue.empty?
|
92
|
+
action, channel, callback = @queue.pop(true)
|
93
|
+
|
94
|
+
case action
|
95
|
+
when :listen
|
96
|
+
pg_conn.exec("LISTEN #{pg_conn.escape_identifier channel}")
|
97
|
+
@event_loop.post(&callback) if callback
|
98
|
+
when :unlisten
|
99
|
+
pg_conn.exec("UNLISTEN #{pg_conn.escape_identifier channel}")
|
100
|
+
when :shutdown
|
101
|
+
throw :shutdown
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
pg_conn.wait_for_notify(1) do |chan, pid, message|
|
106
|
+
broadcast(chan, message)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def shutdown
|
114
|
+
@queue.push([:shutdown])
|
115
|
+
Thread.pass while @thread.alive?
|
116
|
+
end
|
117
|
+
|
118
|
+
def add_channel(channel, on_success)
|
119
|
+
@queue.push([:listen, channel, on_success])
|
120
|
+
end
|
121
|
+
|
122
|
+
def remove_channel(channel)
|
123
|
+
@queue.push([:unlisten, channel])
|
124
|
+
end
|
125
|
+
|
126
|
+
def invoke_callback(*)
|
127
|
+
@event_loop.post { super }
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "thread"
|
4
|
+
|
5
|
+
gem "redis", ">= 3", "< 5"
|
6
|
+
require "redis"
|
7
|
+
|
8
|
+
require "active_support/core_ext/hash/except"
|
9
|
+
|
10
|
+
module ActionCable
|
11
|
+
module SubscriptionAdapter
|
12
|
+
class Redis < Base # :nodoc:
|
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.
|
16
|
+
# This is needed, for example, when using Makara proxies for distributed Redis.
|
17
|
+
cattr_accessor :redis_connector, default: ->(config) do
|
18
|
+
config[:id] ||= "ActionCable-PID-#{$$}"
|
19
|
+
::Redis.new(config.except(:adapter, :channel_prefix))
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(*)
|
23
|
+
super
|
24
|
+
@listener = nil
|
25
|
+
@redis_connection_for_broadcasts = nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def broadcast(channel, payload)
|
29
|
+
redis_connection_for_broadcasts.publish(channel, payload)
|
30
|
+
end
|
31
|
+
|
32
|
+
def subscribe(channel, callback, success_callback = nil)
|
33
|
+
listener.add_subscriber(channel, callback, success_callback)
|
34
|
+
end
|
35
|
+
|
36
|
+
def unsubscribe(channel, callback)
|
37
|
+
listener.remove_subscriber(channel, callback)
|
38
|
+
end
|
39
|
+
|
40
|
+
def shutdown
|
41
|
+
@listener.shutdown if @listener
|
42
|
+
end
|
43
|
+
|
44
|
+
def redis_connection_for_subscriptions
|
45
|
+
redis_connection
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
def listener
|
50
|
+
@listener || @server.mutex.synchronize { @listener ||= Listener.new(self, @server.event_loop) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def redis_connection_for_broadcasts
|
54
|
+
@redis_connection_for_broadcasts || @server.mutex.synchronize do
|
55
|
+
@redis_connection_for_broadcasts ||= redis_connection
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def redis_connection
|
60
|
+
self.class.redis_connector.call(@server.config.cable)
|
61
|
+
end
|
62
|
+
|
63
|
+
class Listener < SubscriberMap
|
64
|
+
def initialize(adapter, event_loop)
|
65
|
+
super()
|
66
|
+
|
67
|
+
@adapter = adapter
|
68
|
+
@event_loop = event_loop
|
69
|
+
|
70
|
+
@subscribe_callbacks = Hash.new { |h, k| h[k] = [] }
|
71
|
+
@subscription_lock = Mutex.new
|
72
|
+
|
73
|
+
@raw_client = nil
|
74
|
+
|
75
|
+
@when_connected = []
|
76
|
+
|
77
|
+
@thread = nil
|
78
|
+
end
|
79
|
+
|
80
|
+
def listen(conn)
|
81
|
+
conn.without_reconnect do
|
82
|
+
original_client = conn.respond_to?(:_client) ? conn._client : conn.client
|
83
|
+
|
84
|
+
conn.subscribe("_action_cable_internal") do |on|
|
85
|
+
on.subscribe do |chan, count|
|
86
|
+
@subscription_lock.synchronize do
|
87
|
+
if count == 1
|
88
|
+
@raw_client = original_client
|
89
|
+
|
90
|
+
until @when_connected.empty?
|
91
|
+
@when_connected.shift.call
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
if callbacks = @subscribe_callbacks[chan]
|
96
|
+
next_callback = callbacks.shift
|
97
|
+
@event_loop.post(&next_callback) if next_callback
|
98
|
+
@subscribe_callbacks.delete(chan) if callbacks.empty?
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
on.message do |chan, message|
|
104
|
+
broadcast(chan, message)
|
105
|
+
end
|
106
|
+
|
107
|
+
on.unsubscribe do |chan, count|
|
108
|
+
if count == 0
|
109
|
+
@subscription_lock.synchronize do
|
110
|
+
@raw_client = nil
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def shutdown
|
119
|
+
@subscription_lock.synchronize do
|
120
|
+
return if @thread.nil?
|
121
|
+
|
122
|
+
when_connected do
|
123
|
+
send_command("unsubscribe")
|
124
|
+
@raw_client = nil
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
Thread.pass while @thread.alive?
|
129
|
+
end
|
130
|
+
|
131
|
+
def add_channel(channel, on_success)
|
132
|
+
@subscription_lock.synchronize do
|
133
|
+
ensure_listener_running
|
134
|
+
@subscribe_callbacks[channel] << on_success
|
135
|
+
when_connected { send_command("subscribe", channel) }
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def remove_channel(channel)
|
140
|
+
@subscription_lock.synchronize do
|
141
|
+
when_connected { send_command("unsubscribe", channel) }
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def invoke_callback(*)
|
146
|
+
@event_loop.post { super }
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
def ensure_listener_running
|
151
|
+
@thread ||= Thread.new do
|
152
|
+
Thread.current.abort_on_exception = true
|
153
|
+
|
154
|
+
conn = @adapter.redis_connection_for_subscriptions
|
155
|
+
listen conn
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def when_connected(&block)
|
160
|
+
if @raw_client
|
161
|
+
block.call
|
162
|
+
else
|
163
|
+
@when_connected << block
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def send_command(*command)
|
168
|
+
@raw_client.write(command)
|
169
|
+
|
170
|
+
very_raw_connection =
|
171
|
+
@raw_client.connection.instance_variable_defined?(:@connection) &&
|
172
|
+
@raw_client.connection.instance_variable_get(:@connection)
|
173
|
+
|
174
|
+
if very_raw_connection && very_raw_connection.respond_to?(:flush)
|
175
|
+
very_raw_connection.flush
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionCable
|
4
|
+
module SubscriptionAdapter
|
5
|
+
class SubscriberMap
|
6
|
+
def initialize
|
7
|
+
@subscribers = Hash.new { |h, k| h[k] = [] }
|
8
|
+
@sync = Mutex.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def add_subscriber(channel, subscriber, on_success)
|
12
|
+
@sync.synchronize do
|
13
|
+
new_channel = !@subscribers.key?(channel)
|
14
|
+
|
15
|
+
@subscribers[channel] << subscriber
|
16
|
+
|
17
|
+
if new_channel
|
18
|
+
add_channel channel, on_success
|
19
|
+
elsif on_success
|
20
|
+
on_success.call
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def remove_subscriber(channel, subscriber)
|
26
|
+
@sync.synchronize do
|
27
|
+
@subscribers[channel].delete(subscriber)
|
28
|
+
|
29
|
+
if @subscribers[channel].empty?
|
30
|
+
@subscribers.delete channel
|
31
|
+
remove_channel channel
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def broadcast(channel, message)
|
37
|
+
list = @sync.synchronize do
|
38
|
+
return if !@subscribers.key?(channel)
|
39
|
+
@subscribers[channel].dup
|
40
|
+
end
|
41
|
+
|
42
|
+
list.each do |subscriber|
|
43
|
+
invoke_callback(subscriber, message)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def add_channel(channel, on_success)
|
48
|
+
on_success.call if on_success
|
49
|
+
end
|
50
|
+
|
51
|
+
def remove_channel(channel)
|
52
|
+
end
|
53
|
+
|
54
|
+
def invoke_callback(callback, message)
|
55
|
+
callback.call message
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|