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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +169 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +24 -0
  5. data/app/assets/javascripts/action_cable.js +517 -0
  6. data/lib/action_cable.rb +62 -0
  7. data/lib/action_cable/channel.rb +17 -0
  8. data/lib/action_cable/channel/base.rb +311 -0
  9. data/lib/action_cable/channel/broadcasting.rb +41 -0
  10. data/lib/action_cable/channel/callbacks.rb +37 -0
  11. data/lib/action_cable/channel/naming.rb +25 -0
  12. data/lib/action_cable/channel/periodic_timers.rb +78 -0
  13. data/lib/action_cable/channel/streams.rb +176 -0
  14. data/lib/action_cable/channel/test_case.rb +310 -0
  15. data/lib/action_cable/connection.rb +22 -0
  16. data/lib/action_cable/connection/authorization.rb +15 -0
  17. data/lib/action_cable/connection/base.rb +264 -0
  18. data/lib/action_cable/connection/client_socket.rb +157 -0
  19. data/lib/action_cable/connection/identification.rb +47 -0
  20. data/lib/action_cable/connection/internal_channel.rb +45 -0
  21. data/lib/action_cable/connection/message_buffer.rb +54 -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 +79 -0
  25. data/lib/action_cable/connection/tagged_logger_proxy.rb +42 -0
  26. data/lib/action_cable/connection/test_case.rb +234 -0
  27. data/lib/action_cable/connection/web_socket.rb +41 -0
  28. data/lib/action_cable/engine.rb +79 -0
  29. data/lib/action_cable/gem_version.rb +17 -0
  30. data/lib/action_cable/helpers/action_cable_helper.rb +42 -0
  31. data/lib/action_cable/remote_connections.rb +71 -0
  32. data/lib/action_cable/server.rb +17 -0
  33. data/lib/action_cable/server/base.rb +94 -0
  34. data/lib/action_cable/server/broadcasting.rb +54 -0
  35. data/lib/action_cable/server/configuration.rb +56 -0
  36. data/lib/action_cable/server/connections.rb +36 -0
  37. data/lib/action_cable/server/worker.rb +75 -0
  38. data/lib/action_cable/server/worker/active_record_connection_management.rb +21 -0
  39. data/lib/action_cable/subscription_adapter.rb +12 -0
  40. data/lib/action_cable/subscription_adapter/async.rb +29 -0
  41. data/lib/action_cable/subscription_adapter/base.rb +30 -0
  42. data/lib/action_cable/subscription_adapter/channel_prefix.rb +28 -0
  43. data/lib/action_cable/subscription_adapter/inline.rb +37 -0
  44. data/lib/action_cable/subscription_adapter/postgresql.rb +132 -0
  45. data/lib/action_cable/subscription_adapter/redis.rb +181 -0
  46. data/lib/action_cable/subscription_adapter/subscriber_map.rb +59 -0
  47. data/lib/action_cable/subscription_adapter/test.rb +40 -0
  48. data/lib/action_cable/test_case.rb +11 -0
  49. data/lib/action_cable/test_helper.rb +133 -0
  50. data/lib/action_cable/version.rb +10 -0
  51. data/lib/rails/generators/channel/USAGE +13 -0
  52. data/lib/rails/generators/channel/channel_generator.rb +52 -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 +5 -0
  59. data/lib/rails/generators/test_unit/channel_generator.rb +20 -0
  60. data/lib/rails/generators/test_unit/templates/channel_test.rb.tt +8 -0
  61. 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