omg-actioncable 8.0.0.alpha2
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 +24 -0
- data/app/assets/javascripts/action_cable.js +511 -0
- data/app/assets/javascripts/actioncable.esm.js +512 -0
- data/app/assets/javascripts/actioncable.js +510 -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 +78 -0
- data/lib/action_cable/channel/streams.rb +215 -0
- data/lib/action_cable/channel/test_case.rb +356 -0
- data/lib/action_cable/connection/authorization.rb +18 -0
- data/lib/action_cable/connection/base.rb +294 -0
- data/lib/action_cable/connection/callbacks.rb +57 -0
- data/lib/action_cable/connection/client_socket.rb +159 -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/message_buffer.rb +57 -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 +85 -0
- data/lib/action_cable/connection/tagged_logger_proxy.rb +47 -0
- data/lib/action_cable/connection/test_case.rb +246 -0
- data/lib/action_cable/connection/web_socket.rb +45 -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 +109 -0
- data/lib/action_cable/server/broadcasting.rb +62 -0
- data/lib/action_cable/server/configuration.rb +70 -0
- data/lib/action_cable/server/connections.rb +44 -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 +29 -0
- data/lib/action_cable/subscription_adapter/base.rb +36 -0
- data/lib/action_cable/subscription_adapter/channel_prefix.rb +30 -0
- data/lib/action_cable/subscription_adapter/inline.rb +39 -0
- data/lib/action_cable/subscription_adapter/postgresql.rb +134 -0
- data/lib/action_cable/subscription_adapter/redis.rb +256 -0
- data/lib/action_cable/subscription_adapter/subscriber_map.rb +61 -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 +80 -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 +181 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# :markup: markdown
|
|
4
|
+
|
|
5
|
+
module ActionCable
|
|
6
|
+
module SubscriptionAdapter
|
|
7
|
+
class Base
|
|
8
|
+
attr_reader :logger, :server
|
|
9
|
+
|
|
10
|
+
def initialize(server)
|
|
11
|
+
@server = server
|
|
12
|
+
@logger = @server.logger
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def broadcast(channel, payload)
|
|
16
|
+
raise NotImplementedError
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def subscribe(channel, message_callback, success_callback = nil)
|
|
20
|
+
raise NotImplementedError
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def unsubscribe(channel, message_callback)
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def shutdown
|
|
28
|
+
raise NotImplementedError
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def identifier
|
|
32
|
+
@server.config.cable[:id] ||= "ActionCable-PID-#{$$}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# :markup: markdown
|
|
4
|
+
|
|
5
|
+
module ActionCable
|
|
6
|
+
module SubscriptionAdapter
|
|
7
|
+
module ChannelPrefix # :nodoc:
|
|
8
|
+
def broadcast(channel, payload)
|
|
9
|
+
channel = channel_with_prefix(channel)
|
|
10
|
+
super
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def subscribe(channel, callback, success_callback = nil)
|
|
14
|
+
channel = channel_with_prefix(channel)
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def unsubscribe(channel, callback)
|
|
19
|
+
channel = channel_with_prefix(channel)
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
# Returns the channel name, including channel_prefix specified in cable.yml
|
|
25
|
+
def channel_with_prefix(channel)
|
|
26
|
+
[@server.config.cable[:channel_prefix], channel].compact.join(":")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# :markup: markdown
|
|
4
|
+
|
|
5
|
+
module ActionCable
|
|
6
|
+
module SubscriptionAdapter
|
|
7
|
+
class Inline < Base # :nodoc:
|
|
8
|
+
def initialize(*)
|
|
9
|
+
super
|
|
10
|
+
@subscriber_map = nil
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def broadcast(channel, payload)
|
|
14
|
+
subscriber_map.broadcast(channel, payload)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def subscribe(channel, callback, success_callback = nil)
|
|
18
|
+
subscriber_map.add_subscriber(channel, callback, success_callback)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def unsubscribe(channel, callback)
|
|
22
|
+
subscriber_map.remove_subscriber(channel, callback)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def shutdown
|
|
26
|
+
# nothing to do
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
def subscriber_map
|
|
31
|
+
@subscriber_map || @server.mutex.synchronize { @subscriber_map ||= new_subscriber_map }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def new_subscriber_map
|
|
35
|
+
SubscriberMap.new
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
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
|
+
@listener = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def broadcast(channel, payload)
|
|
20
|
+
with_broadcast_connection do |pg_conn|
|
|
21
|
+
pg_conn.exec("NOTIFY #{pg_conn.escape_identifier(channel_identifier(channel))}, '#{pg_conn.escape_string(payload)}'")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def subscribe(channel, callback, success_callback = nil)
|
|
26
|
+
listener.add_subscriber(channel_identifier(channel), callback, success_callback)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def unsubscribe(channel, callback)
|
|
30
|
+
listener.remove_subscriber(channel_identifier(channel), callback)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def shutdown
|
|
34
|
+
listener.shutdown
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def with_subscriptions_connection(&block) # :nodoc:
|
|
38
|
+
ar_conn = ActiveRecord::Base.connection_pool.checkout.tap do |conn|
|
|
39
|
+
# Action Cable is taking ownership over this database connection, and will
|
|
40
|
+
# perform the necessary cleanup tasks
|
|
41
|
+
ActiveRecord::Base.connection_pool.remove(conn)
|
|
42
|
+
end
|
|
43
|
+
pg_conn = ar_conn.raw_connection
|
|
44
|
+
|
|
45
|
+
verify!(pg_conn)
|
|
46
|
+
pg_conn.exec("SET application_name = #{pg_conn.escape_identifier(identifier)}")
|
|
47
|
+
yield pg_conn
|
|
48
|
+
ensure
|
|
49
|
+
ar_conn.disconnect!
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def with_broadcast_connection(&block) # :nodoc:
|
|
53
|
+
ActiveRecord::Base.connection_pool.with_connection do |ar_conn|
|
|
54
|
+
pg_conn = ar_conn.raw_connection
|
|
55
|
+
verify!(pg_conn)
|
|
56
|
+
yield pg_conn
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
def channel_identifier(channel)
|
|
62
|
+
channel.size > 63 ? OpenSSL::Digest::SHA1.hexdigest(channel) : channel
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def listener
|
|
66
|
+
@listener || @server.mutex.synchronize { @listener ||= Listener.new(self, @server.event_loop) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def verify!(pg_conn)
|
|
70
|
+
unless pg_conn.is_a?(PG::Connection)
|
|
71
|
+
raise "The Active Record database must be PostgreSQL in order to use the PostgreSQL Action Cable storage adapter"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class Listener < SubscriberMap
|
|
76
|
+
def initialize(adapter, event_loop)
|
|
77
|
+
super()
|
|
78
|
+
|
|
79
|
+
@adapter = adapter
|
|
80
|
+
@event_loop = event_loop
|
|
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
|
+
@event_loop.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
|
+
|
|
128
|
+
def invoke_callback(*)
|
|
129
|
+
@event_loop.post { super }
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,256 @@
|
|
|
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
|
+
@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, config_options, @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(config_options)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def config_options
|
|
64
|
+
@config_options ||= @server.config.cable.deep_symbolize_keys.merge(id: identifier)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
class Listener < SubscriberMap
|
|
68
|
+
def initialize(adapter, config_options, event_loop)
|
|
69
|
+
super()
|
|
70
|
+
|
|
71
|
+
@adapter = adapter
|
|
72
|
+
@event_loop = event_loop
|
|
73
|
+
|
|
74
|
+
@subscribe_callbacks = Hash.new { |h, k| h[k] = [] }
|
|
75
|
+
@subscription_lock = Mutex.new
|
|
76
|
+
|
|
77
|
+
@reconnect_attempt = 0
|
|
78
|
+
# Use the same config as used by Redis conn
|
|
79
|
+
@reconnect_attempts = config_options.fetch(:reconnect_attempts, 1)
|
|
80
|
+
@reconnect_attempts = Array.new(@reconnect_attempts, 0) if @reconnect_attempts.is_a?(Integer)
|
|
81
|
+
|
|
82
|
+
@subscribed_client = nil
|
|
83
|
+
|
|
84
|
+
@when_connected = []
|
|
85
|
+
|
|
86
|
+
@thread = nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def listen(conn)
|
|
90
|
+
conn.without_reconnect do
|
|
91
|
+
original_client = extract_subscribed_client(conn)
|
|
92
|
+
|
|
93
|
+
conn.subscribe("_action_cable_internal") do |on|
|
|
94
|
+
on.subscribe do |chan, count|
|
|
95
|
+
@subscription_lock.synchronize do
|
|
96
|
+
if count == 1
|
|
97
|
+
@reconnect_attempt = 0
|
|
98
|
+
@subscribed_client = original_client
|
|
99
|
+
|
|
100
|
+
until @when_connected.empty?
|
|
101
|
+
@when_connected.shift.call
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if callbacks = @subscribe_callbacks[chan]
|
|
106
|
+
next_callback = callbacks.shift
|
|
107
|
+
@event_loop.post(&next_callback) if next_callback
|
|
108
|
+
@subscribe_callbacks.delete(chan) if callbacks.empty?
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
on.message do |chan, message|
|
|
114
|
+
broadcast(chan, message)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
on.unsubscribe do |chan, count|
|
|
118
|
+
if count == 0
|
|
119
|
+
@subscription_lock.synchronize do
|
|
120
|
+
@subscribed_client = nil
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def shutdown
|
|
129
|
+
@subscription_lock.synchronize do
|
|
130
|
+
return if @thread.nil?
|
|
131
|
+
|
|
132
|
+
when_connected do
|
|
133
|
+
@subscribed_client.unsubscribe
|
|
134
|
+
@subscribed_client = nil
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
Thread.pass while @thread.alive?
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def add_channel(channel, on_success)
|
|
142
|
+
@subscription_lock.synchronize do
|
|
143
|
+
ensure_listener_running
|
|
144
|
+
@subscribe_callbacks[channel] << on_success
|
|
145
|
+
when_connected { @subscribed_client.subscribe(channel) }
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def remove_channel(channel)
|
|
150
|
+
@subscription_lock.synchronize do
|
|
151
|
+
when_connected { @subscribed_client.unsubscribe(channel) }
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def invoke_callback(*)
|
|
156
|
+
@event_loop.post { super }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
private
|
|
160
|
+
def ensure_listener_running
|
|
161
|
+
@thread ||= Thread.new do
|
|
162
|
+
Thread.current.abort_on_exception = true
|
|
163
|
+
|
|
164
|
+
begin
|
|
165
|
+
conn = @adapter.redis_connection_for_subscriptions
|
|
166
|
+
listen conn
|
|
167
|
+
rescue ConnectionError
|
|
168
|
+
reset
|
|
169
|
+
if retry_connecting?
|
|
170
|
+
when_connected { resubscribe }
|
|
171
|
+
retry
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def when_connected(&block)
|
|
178
|
+
if @subscribed_client
|
|
179
|
+
block.call
|
|
180
|
+
else
|
|
181
|
+
@when_connected << block
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def retry_connecting?
|
|
186
|
+
@reconnect_attempt += 1
|
|
187
|
+
|
|
188
|
+
return false if @reconnect_attempt > @reconnect_attempts.size
|
|
189
|
+
|
|
190
|
+
sleep_t = @reconnect_attempts[@reconnect_attempt - 1]
|
|
191
|
+
|
|
192
|
+
sleep(sleep_t) if sleep_t > 0
|
|
193
|
+
|
|
194
|
+
true
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def resubscribe
|
|
198
|
+
channels = @sync.synchronize do
|
|
199
|
+
@subscribers.keys
|
|
200
|
+
end
|
|
201
|
+
@subscribed_client.subscribe(*channels) unless channels.empty?
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def reset
|
|
205
|
+
@subscription_lock.synchronize do
|
|
206
|
+
@subscribed_client = nil
|
|
207
|
+
@subscribe_callbacks.clear
|
|
208
|
+
@when_connected.clear
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
if ::Redis::VERSION < "5"
|
|
213
|
+
ConnectionError = ::Redis::BaseConnectionError
|
|
214
|
+
|
|
215
|
+
class SubscribedClient
|
|
216
|
+
def initialize(raw_client)
|
|
217
|
+
@raw_client = raw_client
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def subscribe(*channel)
|
|
221
|
+
send_command("subscribe", *channel)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def unsubscribe(*channel)
|
|
225
|
+
send_command("unsubscribe", *channel)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
private
|
|
229
|
+
def send_command(*command)
|
|
230
|
+
@raw_client.write(command)
|
|
231
|
+
|
|
232
|
+
very_raw_connection =
|
|
233
|
+
@raw_client.connection.instance_variable_defined?(:@connection) &&
|
|
234
|
+
@raw_client.connection.instance_variable_get(:@connection)
|
|
235
|
+
|
|
236
|
+
if very_raw_connection && very_raw_connection.respond_to?(:flush)
|
|
237
|
+
very_raw_connection.flush
|
|
238
|
+
end
|
|
239
|
+
nil
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def extract_subscribed_client(conn)
|
|
244
|
+
SubscribedClient.new(conn._client)
|
|
245
|
+
end
|
|
246
|
+
else
|
|
247
|
+
ConnectionError = RedisClient::ConnectionError
|
|
248
|
+
|
|
249
|
+
def extract_subscribed_client(conn)
|
|
250
|
+
conn
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
end
|
|
60
|
+
end
|
|
61
|
+
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
|