omg-actioncable 8.0.0.alpha2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +24 -0
  5. data/app/assets/javascripts/action_cable.js +511 -0
  6. data/app/assets/javascripts/actioncable.esm.js +512 -0
  7. data/app/assets/javascripts/actioncable.js +510 -0
  8. data/lib/action_cable/channel/base.rb +335 -0
  9. data/lib/action_cable/channel/broadcasting.rb +50 -0
  10. data/lib/action_cable/channel/callbacks.rb +76 -0
  11. data/lib/action_cable/channel/naming.rb +28 -0
  12. data/lib/action_cable/channel/periodic_timers.rb +78 -0
  13. data/lib/action_cable/channel/streams.rb +215 -0
  14. data/lib/action_cable/channel/test_case.rb +356 -0
  15. data/lib/action_cable/connection/authorization.rb +18 -0
  16. data/lib/action_cable/connection/base.rb +294 -0
  17. data/lib/action_cable/connection/callbacks.rb +57 -0
  18. data/lib/action_cable/connection/client_socket.rb +159 -0
  19. data/lib/action_cable/connection/identification.rb +51 -0
  20. data/lib/action_cable/connection/internal_channel.rb +50 -0
  21. data/lib/action_cable/connection/message_buffer.rb +57 -0
  22. data/lib/action_cable/connection/stream.rb +117 -0
  23. data/lib/action_cable/connection/stream_event_loop.rb +136 -0
  24. data/lib/action_cable/connection/subscriptions.rb +85 -0
  25. data/lib/action_cable/connection/tagged_logger_proxy.rb +47 -0
  26. data/lib/action_cable/connection/test_case.rb +246 -0
  27. data/lib/action_cable/connection/web_socket.rb +45 -0
  28. data/lib/action_cable/deprecator.rb +9 -0
  29. data/lib/action_cable/engine.rb +98 -0
  30. data/lib/action_cable/gem_version.rb +19 -0
  31. data/lib/action_cable/helpers/action_cable_helper.rb +45 -0
  32. data/lib/action_cable/remote_connections.rb +82 -0
  33. data/lib/action_cable/server/base.rb +109 -0
  34. data/lib/action_cable/server/broadcasting.rb +62 -0
  35. data/lib/action_cable/server/configuration.rb +70 -0
  36. data/lib/action_cable/server/connections.rb +44 -0
  37. data/lib/action_cable/server/worker/active_record_connection_management.rb +23 -0
  38. data/lib/action_cable/server/worker.rb +75 -0
  39. data/lib/action_cable/subscription_adapter/async.rb +29 -0
  40. data/lib/action_cable/subscription_adapter/base.rb +36 -0
  41. data/lib/action_cable/subscription_adapter/channel_prefix.rb +30 -0
  42. data/lib/action_cable/subscription_adapter/inline.rb +39 -0
  43. data/lib/action_cable/subscription_adapter/postgresql.rb +134 -0
  44. data/lib/action_cable/subscription_adapter/redis.rb +256 -0
  45. data/lib/action_cable/subscription_adapter/subscriber_map.rb +61 -0
  46. data/lib/action_cable/subscription_adapter/test.rb +41 -0
  47. data/lib/action_cable/test_case.rb +13 -0
  48. data/lib/action_cable/test_helper.rb +163 -0
  49. data/lib/action_cable/version.rb +12 -0
  50. data/lib/action_cable.rb +80 -0
  51. data/lib/rails/generators/channel/USAGE +19 -0
  52. data/lib/rails/generators/channel/channel_generator.rb +127 -0
  53. data/lib/rails/generators/channel/templates/application_cable/channel.rb.tt +4 -0
  54. data/lib/rails/generators/channel/templates/application_cable/connection.rb.tt +4 -0
  55. data/lib/rails/generators/channel/templates/channel.rb.tt +16 -0
  56. data/lib/rails/generators/channel/templates/javascript/channel.js.tt +20 -0
  57. data/lib/rails/generators/channel/templates/javascript/consumer.js.tt +6 -0
  58. data/lib/rails/generators/channel/templates/javascript/index.js.tt +1 -0
  59. data/lib/rails/generators/test_unit/channel_generator.rb +22 -0
  60. data/lib/rails/generators/test_unit/templates/channel_test.rb.tt +8 -0
  61. 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