actioncable-next 0.1.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 +5 -0
- data/MIT-LICENSE +20 -0
- data/README.md +17 -0
- data/lib/action_cable/channel/base.rb +335 -0
- data/lib/action_cable/channel/broadcasting.rb +50 -0
- data/lib/action_cable/channel/callbacks.rb +76 -0
- data/lib/action_cable/channel/naming.rb +28 -0
- data/lib/action_cable/channel/periodic_timers.rb +81 -0
- data/lib/action_cable/channel/streams.rb +213 -0
- data/lib/action_cable/channel/test_case.rb +329 -0
- data/lib/action_cable/connection/authorization.rb +18 -0
- data/lib/action_cable/connection/base.rb +165 -0
- data/lib/action_cable/connection/callbacks.rb +57 -0
- data/lib/action_cable/connection/identification.rb +51 -0
- data/lib/action_cable/connection/internal_channel.rb +50 -0
- data/lib/action_cable/connection/subscriptions.rb +124 -0
- data/lib/action_cable/connection/test_case.rb +294 -0
- data/lib/action_cable/deprecator.rb +9 -0
- data/lib/action_cable/engine.rb +98 -0
- data/lib/action_cable/gem_version.rb +19 -0
- data/lib/action_cable/helpers/action_cable_helper.rb +45 -0
- data/lib/action_cable/remote_connections.rb +82 -0
- data/lib/action_cable/server/base.rb +163 -0
- data/lib/action_cable/server/broadcasting.rb +62 -0
- data/lib/action_cable/server/configuration.rb +75 -0
- data/lib/action_cable/server/connections.rb +44 -0
- data/lib/action_cable/server/socket/client_socket.rb +159 -0
- data/lib/action_cable/server/socket/message_buffer.rb +56 -0
- data/lib/action_cable/server/socket/stream.rb +117 -0
- data/lib/action_cable/server/socket/web_socket.rb +47 -0
- data/lib/action_cable/server/socket.rb +180 -0
- data/lib/action_cable/server/stream_event_loop.rb +119 -0
- data/lib/action_cable/server/tagged_logger_proxy.rb +46 -0
- data/lib/action_cable/server/worker/active_record_connection_management.rb +23 -0
- data/lib/action_cable/server/worker.rb +75 -0
- data/lib/action_cable/subscription_adapter/async.rb +14 -0
- data/lib/action_cable/subscription_adapter/base.rb +39 -0
- data/lib/action_cable/subscription_adapter/channel_prefix.rb +30 -0
- data/lib/action_cable/subscription_adapter/inline.rb +40 -0
- data/lib/action_cable/subscription_adapter/postgresql.rb +130 -0
- data/lib/action_cable/subscription_adapter/redis.rb +257 -0
- data/lib/action_cable/subscription_adapter/subscriber_map.rb +80 -0
- data/lib/action_cable/subscription_adapter/test.rb +41 -0
- data/lib/action_cable/test_case.rb +13 -0
- data/lib/action_cable/test_helper.rb +163 -0
- data/lib/action_cable/version.rb +12 -0
- data/lib/action_cable.rb +81 -0
- data/lib/actioncable-next.rb +5 -0
- data/lib/rails/generators/channel/USAGE +19 -0
- data/lib/rails/generators/channel/channel_generator.rb +127 -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 +1 -0
- data/lib/rails/generators/test_unit/channel_generator.rb +22 -0
- data/lib/rails/generators/test_unit/templates/channel_test.rb.tt +8 -0
- metadata +191 -0
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
gem "pg", "~> 1.1"
|
6
|
+
require "pg"
|
7
|
+
require "openssl"
|
8
|
+
|
9
|
+
module ActionCable
|
10
|
+
module SubscriptionAdapter
|
11
|
+
class PostgreSQL < Base # :nodoc:
|
12
|
+
prepend ChannelPrefix
|
13
|
+
|
14
|
+
def initialize(*)
|
15
|
+
super
|
16
|
+
@mutex = Mutex.new
|
17
|
+
@listener = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def broadcast(channel, payload)
|
21
|
+
with_broadcast_connection do |pg_conn|
|
22
|
+
pg_conn.exec("NOTIFY #{pg_conn.escape_identifier(channel_identifier(channel))}, '#{pg_conn.escape_string(payload)}'")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def subscribe(channel, callback, success_callback = nil)
|
27
|
+
listener.add_subscriber(channel_identifier(channel), callback, success_callback)
|
28
|
+
end
|
29
|
+
|
30
|
+
def unsubscribe(channel, callback)
|
31
|
+
listener.remove_subscriber(channel_identifier(channel), callback)
|
32
|
+
end
|
33
|
+
|
34
|
+
def shutdown
|
35
|
+
listener.shutdown
|
36
|
+
end
|
37
|
+
|
38
|
+
def with_subscriptions_connection(&block) # :nodoc:
|
39
|
+
ar_conn = ActiveRecord::Base.connection_pool.checkout.tap do |conn|
|
40
|
+
# Action Cable is taking ownership over this database connection, and will
|
41
|
+
# perform the necessary cleanup tasks
|
42
|
+
ActiveRecord::Base.connection_pool.remove(conn)
|
43
|
+
end
|
44
|
+
pg_conn = ar_conn.raw_connection
|
45
|
+
|
46
|
+
verify!(pg_conn)
|
47
|
+
pg_conn.exec("SET application_name = #{pg_conn.escape_identifier(identifier)}")
|
48
|
+
yield pg_conn
|
49
|
+
ensure
|
50
|
+
ar_conn.disconnect!
|
51
|
+
end
|
52
|
+
|
53
|
+
def with_broadcast_connection(&block) # :nodoc:
|
54
|
+
ActiveRecord::Base.connection_pool.with_connection do |ar_conn|
|
55
|
+
pg_conn = ar_conn.raw_connection
|
56
|
+
verify!(pg_conn)
|
57
|
+
yield pg_conn
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
def channel_identifier(channel)
|
63
|
+
channel.size > 63 ? OpenSSL::Digest::SHA1.hexdigest(channel) : channel
|
64
|
+
end
|
65
|
+
|
66
|
+
def listener
|
67
|
+
@listener || @mutex.synchronize { @listener ||= Listener.new(self, executor) }
|
68
|
+
end
|
69
|
+
|
70
|
+
def verify!(pg_conn)
|
71
|
+
unless pg_conn.is_a?(PG::Connection)
|
72
|
+
raise "The Active Record database must be PostgreSQL in order to use the PostgreSQL Action Cable storage adapter"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class Listener < SubscriberMap::Async
|
77
|
+
def initialize(adapter, executor)
|
78
|
+
super(executor)
|
79
|
+
|
80
|
+
@adapter = adapter
|
81
|
+
@queue = Queue.new
|
82
|
+
|
83
|
+
@thread = Thread.new do
|
84
|
+
Thread.current.abort_on_exception = true
|
85
|
+
listen
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def listen
|
90
|
+
@adapter.with_subscriptions_connection do |pg_conn|
|
91
|
+
catch :shutdown do
|
92
|
+
loop do
|
93
|
+
until @queue.empty?
|
94
|
+
action, channel, callback = @queue.pop(true)
|
95
|
+
|
96
|
+
case action
|
97
|
+
when :listen
|
98
|
+
pg_conn.exec("LISTEN #{pg_conn.escape_identifier channel}")
|
99
|
+
@executor.post(&callback) if callback
|
100
|
+
when :unlisten
|
101
|
+
pg_conn.exec("UNLISTEN #{pg_conn.escape_identifier channel}")
|
102
|
+
when :shutdown
|
103
|
+
throw :shutdown
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
pg_conn.wait_for_notify(1) do |chan, pid, message|
|
108
|
+
broadcast(chan, message)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def shutdown
|
116
|
+
@queue.push([:shutdown])
|
117
|
+
Thread.pass while @thread.alive?
|
118
|
+
end
|
119
|
+
|
120
|
+
def add_channel(channel, on_success)
|
121
|
+
@queue.push([:listen, channel, on_success])
|
122
|
+
end
|
123
|
+
|
124
|
+
def remove_channel(channel)
|
125
|
+
@queue.push([:unlisten, channel])
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,257 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
gem "redis", ">= 4", "< 6"
|
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
|
16
|
+
# different Redis library than the redis gem. This is needed, for example, when
|
17
|
+
# using Makara proxies for distributed Redis.
|
18
|
+
cattr_accessor :redis_connector, default: ->(config) do
|
19
|
+
::Redis.new(config.except(:adapter, :channel_prefix))
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(*)
|
23
|
+
super
|
24
|
+
@listener = nil
|
25
|
+
@mutex = Mutex.new
|
26
|
+
@redis_connection_for_broadcasts = nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def broadcast(channel, payload)
|
30
|
+
redis_connection_for_broadcasts.publish(channel, payload)
|
31
|
+
end
|
32
|
+
|
33
|
+
def subscribe(channel, callback, success_callback = nil)
|
34
|
+
listener.add_subscriber(channel, callback, success_callback)
|
35
|
+
end
|
36
|
+
|
37
|
+
def unsubscribe(channel, callback)
|
38
|
+
listener.remove_subscriber(channel, callback)
|
39
|
+
end
|
40
|
+
|
41
|
+
def shutdown
|
42
|
+
@listener.shutdown if @listener
|
43
|
+
end
|
44
|
+
|
45
|
+
def redis_connection_for_subscriptions
|
46
|
+
redis_connection
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
def listener
|
51
|
+
@listener || @mutex.synchronize { @listener ||= Listener.new(self, config_options, executor) }
|
52
|
+
end
|
53
|
+
|
54
|
+
def redis_connection_for_broadcasts
|
55
|
+
@redis_connection_for_broadcasts || @mutex.synchronize do
|
56
|
+
@redis_connection_for_broadcasts ||= redis_connection
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def redis_connection
|
61
|
+
self.class.redis_connector.call(config_options)
|
62
|
+
end
|
63
|
+
|
64
|
+
def config_options
|
65
|
+
@config_options ||= config.cable.deep_symbolize_keys.merge(id: identifier)
|
66
|
+
end
|
67
|
+
|
68
|
+
class Listener < SubscriberMap::Async
|
69
|
+
delegate :logger, to: :@adapter
|
70
|
+
|
71
|
+
def initialize(adapter, config_options, executor)
|
72
|
+
super(executor)
|
73
|
+
|
74
|
+
@adapter = adapter
|
75
|
+
|
76
|
+
@subscribe_callbacks = Hash.new { |h, k| h[k] = [] }
|
77
|
+
@subscription_lock = Mutex.new
|
78
|
+
|
79
|
+
@reconnect_attempt = 0
|
80
|
+
# Use the same config as used by Redis conn
|
81
|
+
@reconnect_attempts = config_options.fetch(:reconnect_attempts, 1)
|
82
|
+
@reconnect_attempts = Array.new(@reconnect_attempts, 0) if @reconnect_attempts.is_a?(Integer)
|
83
|
+
|
84
|
+
@subscribed_client = nil
|
85
|
+
|
86
|
+
@when_connected = []
|
87
|
+
|
88
|
+
@thread = nil
|
89
|
+
end
|
90
|
+
|
91
|
+
def listen(conn)
|
92
|
+
conn.without_reconnect do
|
93
|
+
original_client = extract_subscribed_client(conn)
|
94
|
+
|
95
|
+
conn.subscribe("_action_cable_internal") do |on|
|
96
|
+
on.subscribe do |chan, count|
|
97
|
+
@subscription_lock.synchronize do
|
98
|
+
if count == 1
|
99
|
+
@reconnect_attempt = 0
|
100
|
+
@subscribed_client = original_client
|
101
|
+
|
102
|
+
until @when_connected.empty?
|
103
|
+
@when_connected.shift.call
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
if callbacks = @subscribe_callbacks[chan]
|
108
|
+
next_callback = callbacks.shift
|
109
|
+
@executor.post(&next_callback) if next_callback
|
110
|
+
@subscribe_callbacks.delete(chan) if callbacks.empty?
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
on.message do |chan, message|
|
116
|
+
broadcast(chan, message)
|
117
|
+
end
|
118
|
+
|
119
|
+
on.unsubscribe do |chan, count|
|
120
|
+
if count == 0
|
121
|
+
@subscription_lock.synchronize do
|
122
|
+
@subscribed_client = nil
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def shutdown
|
131
|
+
@subscription_lock.synchronize do
|
132
|
+
return if @thread.nil?
|
133
|
+
|
134
|
+
when_connected do
|
135
|
+
@subscribed_client.unsubscribe
|
136
|
+
@subscribed_client = nil
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
Thread.pass while @thread.alive?
|
141
|
+
end
|
142
|
+
|
143
|
+
def add_channel(channel, on_success)
|
144
|
+
@subscription_lock.synchronize do
|
145
|
+
ensure_listener_running
|
146
|
+
@subscribe_callbacks[channel] << on_success
|
147
|
+
when_connected { @subscribed_client.subscribe(channel) }
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def remove_channel(channel)
|
152
|
+
@subscription_lock.synchronize do
|
153
|
+
when_connected { @subscribed_client.unsubscribe(channel) }
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
def ensure_listener_running
|
159
|
+
@thread ||= Thread.new do
|
160
|
+
Thread.current.abort_on_exception = true
|
161
|
+
|
162
|
+
begin
|
163
|
+
conn = @adapter.redis_connection_for_subscriptions
|
164
|
+
listen conn
|
165
|
+
rescue ConnectionError => e
|
166
|
+
reset
|
167
|
+
if retry_connecting?
|
168
|
+
logger&.warn "Redis connection failed: #{e.message}. Trying to reconnect..."
|
169
|
+
when_connected { resubscribe }
|
170
|
+
retry
|
171
|
+
else
|
172
|
+
logger&.error "Failed to reconnect to Redis after #{@reconnect_attempt} attempts."
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def when_connected(&block)
|
179
|
+
if @subscribed_client
|
180
|
+
block.call
|
181
|
+
else
|
182
|
+
@when_connected << block
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def retry_connecting?
|
187
|
+
@reconnect_attempt += 1
|
188
|
+
|
189
|
+
return false if @reconnect_attempt > @reconnect_attempts.size
|
190
|
+
|
191
|
+
sleep_t = @reconnect_attempts[@reconnect_attempt - 1]
|
192
|
+
|
193
|
+
sleep(sleep_t) if sleep_t > 0
|
194
|
+
|
195
|
+
true
|
196
|
+
end
|
197
|
+
|
198
|
+
def resubscribe
|
199
|
+
channels = @sync.synchronize do
|
200
|
+
@subscribers.keys
|
201
|
+
end
|
202
|
+
@subscribed_client.subscribe(*channels) unless channels.empty?
|
203
|
+
end
|
204
|
+
|
205
|
+
def reset
|
206
|
+
@subscription_lock.synchronize do
|
207
|
+
@subscribed_client = nil
|
208
|
+
@subscribe_callbacks.clear
|
209
|
+
@when_connected.clear
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
if ::Redis::VERSION < "5"
|
214
|
+
ConnectionError = ::Redis::BaseConnectionError
|
215
|
+
|
216
|
+
class SubscribedClient
|
217
|
+
def initialize(raw_client)
|
218
|
+
@raw_client = raw_client
|
219
|
+
end
|
220
|
+
|
221
|
+
def subscribe(*channel)
|
222
|
+
send_command("subscribe", *channel)
|
223
|
+
end
|
224
|
+
|
225
|
+
def unsubscribe(*channel)
|
226
|
+
send_command("unsubscribe", *channel)
|
227
|
+
end
|
228
|
+
|
229
|
+
private
|
230
|
+
def send_command(*command)
|
231
|
+
@raw_client.write(command)
|
232
|
+
|
233
|
+
very_raw_connection =
|
234
|
+
@raw_client.connection.instance_variable_defined?(:@connection) &&
|
235
|
+
@raw_client.connection.instance_variable_get(:@connection)
|
236
|
+
|
237
|
+
if very_raw_connection && very_raw_connection.respond_to?(:flush)
|
238
|
+
very_raw_connection.flush
|
239
|
+
end
|
240
|
+
nil
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def extract_subscribed_client(conn)
|
245
|
+
SubscribedClient.new(conn._client)
|
246
|
+
end
|
247
|
+
else
|
248
|
+
ConnectionError = RedisClient::ConnectionError
|
249
|
+
|
250
|
+
def extract_subscribed_client(conn)
|
251
|
+
conn
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
module ActionCable
|
6
|
+
module SubscriptionAdapter
|
7
|
+
class SubscriberMap
|
8
|
+
def initialize
|
9
|
+
@subscribers = Hash.new { |h, k| h[k] = [] }
|
10
|
+
@sync = Mutex.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def add_subscriber(channel, subscriber, on_success)
|
14
|
+
@sync.synchronize do
|
15
|
+
new_channel = !@subscribers.key?(channel)
|
16
|
+
|
17
|
+
@subscribers[channel] << subscriber
|
18
|
+
|
19
|
+
if new_channel
|
20
|
+
add_channel channel, on_success
|
21
|
+
elsif on_success
|
22
|
+
on_success.call
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def remove_subscriber(channel, subscriber)
|
28
|
+
@sync.synchronize do
|
29
|
+
@subscribers[channel].delete(subscriber)
|
30
|
+
|
31
|
+
if @subscribers[channel].empty?
|
32
|
+
@subscribers.delete channel
|
33
|
+
remove_channel channel
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def broadcast(channel, message)
|
39
|
+
list = @sync.synchronize do
|
40
|
+
return if !@subscribers.key?(channel)
|
41
|
+
@subscribers[channel].dup
|
42
|
+
end
|
43
|
+
|
44
|
+
list.each do |subscriber|
|
45
|
+
invoke_callback(subscriber, message)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def add_channel(channel, on_success)
|
50
|
+
on_success.call if on_success
|
51
|
+
end
|
52
|
+
|
53
|
+
def remove_channel(channel)
|
54
|
+
end
|
55
|
+
|
56
|
+
def invoke_callback(callback, message)
|
57
|
+
callback.call message
|
58
|
+
end
|
59
|
+
|
60
|
+
class Async < self
|
61
|
+
def initialize(executor)
|
62
|
+
@executor = executor
|
63
|
+
super()
|
64
|
+
end
|
65
|
+
|
66
|
+
def add_subscriber(*)
|
67
|
+
@executor.post { super }
|
68
|
+
end
|
69
|
+
|
70
|
+
def remove_subscriber(*)
|
71
|
+
@executor.post { super }
|
72
|
+
end
|
73
|
+
|
74
|
+
def invoke_callback(*)
|
75
|
+
@executor.post { super }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
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
|
+
# ActionCable::TestHelper it makes a great tool to test your Rails application.
|
11
|
+
#
|
12
|
+
# To use the test adapter set `adapter` value to `test` in your
|
13
|
+
# `config/cable.yml` file.
|
14
|
+
#
|
15
|
+
# NOTE: `Test` adapter extends the `ActionCable::SubscriptionAdapter::Async`
|
16
|
+
# adapter, so it could be used in system tests too.
|
17
|
+
class Test < Async
|
18
|
+
def broadcast(channel, payload)
|
19
|
+
broadcasts(channel) << payload
|
20
|
+
super
|
21
|
+
end
|
22
|
+
|
23
|
+
def broadcasts(channel)
|
24
|
+
channels_data[channel] ||= []
|
25
|
+
end
|
26
|
+
|
27
|
+
def clear_messages(channel)
|
28
|
+
channels_data[channel] = []
|
29
|
+
end
|
30
|
+
|
31
|
+
def clear
|
32
|
+
@channels_data = nil
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def channels_data
|
37
|
+
@channels_data ||= {}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
require "active_support/test_case"
|
6
|
+
|
7
|
+
module ActionCable
|
8
|
+
class TestCase < ActiveSupport::TestCase
|
9
|
+
include ActionCable::TestHelper
|
10
|
+
|
11
|
+
ActiveSupport.run_load_hooks(:action_cable_test_case, self)
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
module ActionCable
|
6
|
+
# Provides helper methods for testing Action Cable broadcasting
|
7
|
+
module TestHelper
|
8
|
+
def before_setup # :nodoc:
|
9
|
+
server = ActionCable.server
|
10
|
+
test_adapter = ActionCable::SubscriptionAdapter::Test.new(server)
|
11
|
+
|
12
|
+
@old_pubsub_adapter = server.pubsub
|
13
|
+
|
14
|
+
server.instance_variable_set(:@pubsub, test_adapter)
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
def after_teardown # :nodoc:
|
19
|
+
super
|
20
|
+
ActionCable.server.instance_variable_set(:@pubsub, @old_pubsub_adapter)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Asserts that the number of broadcasted messages to the stream matches the
|
24
|
+
# given number.
|
25
|
+
#
|
26
|
+
# def test_broadcasts
|
27
|
+
# assert_broadcasts 'messages', 0
|
28
|
+
# ActionCable.server.broadcast 'messages', { text: 'hello' }
|
29
|
+
# assert_broadcasts 'messages', 1
|
30
|
+
# ActionCable.server.broadcast 'messages', { text: 'world' }
|
31
|
+
# assert_broadcasts 'messages', 2
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# If a block is passed, that block should cause the specified number of messages
|
35
|
+
# to be broadcasted.
|
36
|
+
#
|
37
|
+
# def test_broadcasts_again
|
38
|
+
# assert_broadcasts('messages', 1) do
|
39
|
+
# ActionCable.server.broadcast 'messages', { text: 'hello' }
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# assert_broadcasts('messages', 2) do
|
43
|
+
# ActionCable.server.broadcast 'messages', { text: 'hi' }
|
44
|
+
# ActionCable.server.broadcast 'messages', { text: 'how are you?' }
|
45
|
+
# end
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
def assert_broadcasts(stream, number, &block)
|
49
|
+
if block_given?
|
50
|
+
new_messages = new_broadcasts_from(broadcasts(stream), stream, "assert_broadcasts", &block)
|
51
|
+
|
52
|
+
actual_count = new_messages.size
|
53
|
+
assert_equal number, actual_count, "#{number} broadcasts to #{stream} expected, but #{actual_count} were sent"
|
54
|
+
else
|
55
|
+
actual_count = broadcasts(stream).size
|
56
|
+
assert_equal number, actual_count, "#{number} broadcasts to #{stream} expected, but #{actual_count} were sent"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Asserts that no messages have been sent to the stream.
|
61
|
+
#
|
62
|
+
# def test_no_broadcasts
|
63
|
+
# assert_no_broadcasts 'messages'
|
64
|
+
# ActionCable.server.broadcast 'messages', { text: 'hi' }
|
65
|
+
# assert_broadcasts 'messages', 1
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
# If a block is passed, that block should not cause any message to be sent.
|
69
|
+
#
|
70
|
+
# def test_broadcasts_again
|
71
|
+
# assert_no_broadcasts 'messages' do
|
72
|
+
# # No job messages should be sent from this block
|
73
|
+
# end
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# Note: This assertion is simply a shortcut for:
|
77
|
+
#
|
78
|
+
# assert_broadcasts 'messages', 0, &block
|
79
|
+
#
|
80
|
+
def assert_no_broadcasts(stream, &block)
|
81
|
+
assert_broadcasts stream, 0, &block
|
82
|
+
end
|
83
|
+
|
84
|
+
# Returns the messages that are broadcasted in the block.
|
85
|
+
#
|
86
|
+
# def test_broadcasts
|
87
|
+
# messages = capture_broadcasts('messages') do
|
88
|
+
# ActionCable.server.broadcast 'messages', { text: 'hi' }
|
89
|
+
# ActionCable.server.broadcast 'messages', { text: 'how are you?' }
|
90
|
+
# end
|
91
|
+
# assert_equal 2, messages.length
|
92
|
+
# assert_equal({ text: 'hi' }, messages.first)
|
93
|
+
# assert_equal({ text: 'how are you?' }, messages.last)
|
94
|
+
# end
|
95
|
+
#
|
96
|
+
def capture_broadcasts(stream, &block)
|
97
|
+
new_broadcasts_from(broadcasts(stream), stream, "capture_broadcasts", &block).map { |m| ActiveSupport::JSON.decode(m) }
|
98
|
+
end
|
99
|
+
|
100
|
+
# Asserts that the specified message has been sent to the stream.
|
101
|
+
#
|
102
|
+
# def test_assert_transmitted_message
|
103
|
+
# ActionCable.server.broadcast 'messages', text: 'hello'
|
104
|
+
# assert_broadcast_on('messages', text: 'hello')
|
105
|
+
# end
|
106
|
+
#
|
107
|
+
# If a block is passed, that block should cause a message with the specified
|
108
|
+
# data to be sent.
|
109
|
+
#
|
110
|
+
# def test_assert_broadcast_on_again
|
111
|
+
# assert_broadcast_on('messages', text: 'hello') do
|
112
|
+
# ActionCable.server.broadcast 'messages', text: 'hello'
|
113
|
+
# end
|
114
|
+
# end
|
115
|
+
#
|
116
|
+
def assert_broadcast_on(stream, data, &block)
|
117
|
+
# Encode to JSON and back–we want to use this value to compare with decoded
|
118
|
+
# JSON. Comparing JSON strings doesn't work due to the order if the keys.
|
119
|
+
serialized_msg =
|
120
|
+
ActiveSupport::JSON.decode(ActiveSupport::JSON.encode(data))
|
121
|
+
|
122
|
+
new_messages = broadcasts(stream)
|
123
|
+
if block_given?
|
124
|
+
new_messages = new_broadcasts_from(new_messages, stream, "assert_broadcast_on", &block)
|
125
|
+
end
|
126
|
+
|
127
|
+
message = new_messages.find { |msg| ActiveSupport::JSON.decode(msg) == serialized_msg }
|
128
|
+
|
129
|
+
error_message = "No messages sent with #{data} to #{stream}"
|
130
|
+
|
131
|
+
if new_messages.any?
|
132
|
+
error_message = new_messages.inject("#{error_message}\nMessage(s) found:\n") do |error_message, new_message|
|
133
|
+
error_message + "#{ActiveSupport::JSON.decode(new_message)}\n"
|
134
|
+
end
|
135
|
+
else
|
136
|
+
error_message = "#{error_message}\nNo message found for #{stream}"
|
137
|
+
end
|
138
|
+
|
139
|
+
assert message, error_message
|
140
|
+
end
|
141
|
+
|
142
|
+
def pubsub_adapter # :nodoc:
|
143
|
+
ActionCable.server.pubsub
|
144
|
+
end
|
145
|
+
|
146
|
+
delegate :broadcasts, :clear_messages, to: :pubsub_adapter
|
147
|
+
|
148
|
+
private
|
149
|
+
def new_broadcasts_from(current_messages, stream, assertion, &block)
|
150
|
+
old_messages = current_messages
|
151
|
+
clear_messages(stream)
|
152
|
+
|
153
|
+
_assert_nothing_raised_or_warn(assertion, &block)
|
154
|
+
new_messages = broadcasts(stream)
|
155
|
+
clear_messages(stream)
|
156
|
+
|
157
|
+
# Restore all sent messages
|
158
|
+
(old_messages + new_messages).each { |m| pubsub_adapter.broadcast(stream, m) }
|
159
|
+
|
160
|
+
new_messages
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|