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.
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