actioncable 6.0.0

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