actioncable 6.1.7.8 → 7.0.8.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +86 -67
  3. data/README.md +1 -1
  4. data/app/assets/javascripts/action_cable.js +211 -295
  5. data/app/assets/javascripts/actioncable.esm.js +491 -0
  6. data/app/assets/javascripts/actioncable.js +489 -0
  7. data/lib/action_cable/channel/base.rb +1 -1
  8. data/lib/action_cable/channel/broadcasting.rb +1 -1
  9. data/lib/action_cable/channel/naming.rb +1 -1
  10. data/lib/action_cable/channel/streams.rb +5 -7
  11. data/lib/action_cable/channel/test_case.rb +16 -1
  12. data/lib/action_cable/connection/base.rb +5 -5
  13. data/lib/action_cable/connection/identification.rb +1 -1
  14. data/lib/action_cable/connection/subscriptions.rb +1 -1
  15. data/lib/action_cable/connection/tagged_logger_proxy.rb +3 -3
  16. data/lib/action_cable/connection/test_case.rb +1 -1
  17. data/lib/action_cable/engine.rb +10 -1
  18. data/lib/action_cable/gem_version.rb +5 -5
  19. data/lib/action_cable/helpers/action_cable_helper.rb +3 -2
  20. data/lib/action_cable/server/configuration.rb +1 -0
  21. data/lib/action_cable/server/worker/active_record_connection_management.rb +2 -2
  22. data/lib/action_cable/server/worker.rb +3 -4
  23. data/lib/action_cable/subscription_adapter/postgresql.rb +2 -2
  24. data/lib/action_cable/subscription_adapter/redis.rb +98 -22
  25. data/lib/action_cable/subscription_adapter/test.rb +1 -1
  26. data/lib/action_cable/test_helper.rb +2 -2
  27. data/lib/action_cable/version.rb +1 -1
  28. data/lib/rails/generators/channel/USAGE +1 -1
  29. data/lib/rails/generators/channel/channel_generator.rb +79 -20
  30. data/lib/rails/generators/channel/templates/javascript/index.js.tt +1 -5
  31. metadata +18 -16
  32. /data/lib/rails/generators/channel/templates/application_cable/{channel.rb.tt → channel.rb} +0 -0
  33. /data/lib/rails/generators/channel/templates/application_cable/{connection.rb.tt → connection.rb} +0 -0
@@ -1,16 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionCable
4
- # Returns the version of the currently loaded Action Cable as a <tt>Gem::Version</tt>.
4
+ # Returns the currently loaded version of Action Cable as a <tt>Gem::Version</tt>.
5
5
  def self.gem_version
6
6
  Gem::Version.new VERSION::STRING
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 6
11
- MINOR = 1
12
- TINY = 7
13
- PRE = "8"
10
+ MAJOR = 7
11
+ MINOR = 0
12
+ TINY = 8
13
+ PRE = "6"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -8,14 +8,15 @@ module ActionCable
8
8
  #
9
9
  # <head>
10
10
  # <%= action_cable_meta_tag %>
11
- # <%= javascript_include_tag 'application', 'data-turbolinks-track' => 'reload' %>
11
+ # <%= javascript_include_tag 'application', 'data-turbo-track' => 'reload' %>
12
12
  # </head>
13
13
  #
14
14
  # This is then used by Action Cable to determine the URL of your WebSocket server.
15
15
  # Your JavaScript can then connect to the server without needing to specify the
16
16
  # URL directly:
17
17
  #
18
- # window.Cable = require("@rails/actioncable")
18
+ # import Cable from "@rails/actioncable"
19
+ # window.Cable = Cable
19
20
  # window.App = {}
20
21
  # App.cable = Cable.createConsumer()
21
22
  #
@@ -9,6 +9,7 @@ module ActionCable
9
9
  attr_accessor :connection_class, :worker_pool_size
10
10
  attr_accessor :disable_request_forgery_protection, :allowed_request_origins, :allow_same_origin_as_host
11
11
  attr_accessor :cable, :url, :mount_path
12
+ attr_accessor :precompile_assets
12
13
 
13
14
  def initialize
14
15
  @log_tags = []
@@ -12,8 +12,8 @@ module ActionCable
12
12
  end
13
13
  end
14
14
 
15
- def with_database_connections
16
- connection.logger.tag(ActiveRecord::Base.logger) { yield }
15
+ def with_database_connections(&block)
16
+ connection.logger.tag(ActiveRecord::Base.logger, &block)
17
17
  end
18
18
  end
19
19
  end
@@ -19,6 +19,7 @@ module ActionCable
19
19
 
20
20
  def initialize(max_size: 5)
21
21
  @executor = Concurrent::ThreadPoolExecutor.new(
22
+ name: "ActionCable",
22
23
  min_threads: 1,
23
24
  max_threads: max_size,
24
25
  max_queue: 0,
@@ -35,12 +36,10 @@ module ActionCable
35
36
  @executor.shuttingdown?
36
37
  end
37
38
 
38
- def work(connection)
39
+ def work(connection, &block)
39
40
  self.connection = connection
40
41
 
41
- run_callbacks :work do
42
- yield
43
- end
42
+ run_callbacks :work, &block
44
43
  ensure
45
44
  self.connection = nil
46
45
  end
@@ -3,7 +3,7 @@
3
3
  gem "pg", "~> 1.1"
4
4
  require "pg"
5
5
  require "thread"
6
- require "digest/sha1"
6
+ require "openssl"
7
7
 
8
8
  module ActionCable
9
9
  module SubscriptionAdapter
@@ -58,7 +58,7 @@ module ActionCable
58
58
 
59
59
  private
60
60
  def channel_identifier(channel)
61
- channel.size > 63 ? Digest::SHA1.hexdigest(channel) : channel
61
+ channel.size > 63 ? OpenSSL::Digest::SHA1.hexdigest(channel) : channel
62
62
  end
63
63
 
64
64
  def listener
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "thread"
4
4
 
5
- gem "redis", ">= 3", "< 5"
5
+ gem "redis", ">= 3", "< 6"
6
6
  require "redis"
7
7
 
8
8
  require "active_support/core_ext/hash/except"
@@ -46,7 +46,7 @@ module ActionCable
46
46
 
47
47
  private
48
48
  def listener
49
- @listener || @server.mutex.synchronize { @listener ||= Listener.new(self, @server.event_loop) }
49
+ @listener || @server.mutex.synchronize { @listener ||= Listener.new(self, config_options, @server.event_loop) }
50
50
  end
51
51
 
52
52
  def redis_connection_for_broadcasts
@@ -56,11 +56,15 @@ module ActionCable
56
56
  end
57
57
 
58
58
  def redis_connection
59
- self.class.redis_connector.call(@server.config.cable.merge(id: identifier))
59
+ self.class.redis_connector.call(config_options)
60
+ end
61
+
62
+ def config_options
63
+ @config_options ||= @server.config.cable.deep_symbolize_keys.merge(id: identifier)
60
64
  end
61
65
 
62
66
  class Listener < SubscriberMap
63
- def initialize(adapter, event_loop)
67
+ def initialize(adapter, config_options, event_loop)
64
68
  super()
65
69
 
66
70
  @adapter = adapter
@@ -69,7 +73,12 @@ module ActionCable
69
73
  @subscribe_callbacks = Hash.new { |h, k| h[k] = [] }
70
74
  @subscription_lock = Mutex.new
71
75
 
72
- @raw_client = nil
76
+ @reconnect_attempt = 0
77
+ # Use the same config as used by Redis conn
78
+ @reconnect_attempts = config_options.fetch(:reconnect_attempts, 1)
79
+ @reconnect_attempts = Array.new(@reconnect_attempts, 0) if @reconnect_attempts.is_a?(Integer)
80
+
81
+ @subscribed_client = nil
73
82
 
74
83
  @when_connected = []
75
84
 
@@ -78,13 +87,14 @@ module ActionCable
78
87
 
79
88
  def listen(conn)
80
89
  conn.without_reconnect do
81
- original_client = conn.respond_to?(:_client) ? conn._client : conn.client
90
+ original_client = extract_subscribed_client(conn)
82
91
 
83
92
  conn.subscribe("_action_cable_internal") do |on|
84
93
  on.subscribe do |chan, count|
85
94
  @subscription_lock.synchronize do
86
95
  if count == 1
87
- @raw_client = original_client
96
+ @reconnect_attempt = 0
97
+ @subscribed_client = original_client
88
98
 
89
99
  until @when_connected.empty?
90
100
  @when_connected.shift.call
@@ -106,7 +116,7 @@ module ActionCable
106
116
  on.unsubscribe do |chan, count|
107
117
  if count == 0
108
118
  @subscription_lock.synchronize do
109
- @raw_client = nil
119
+ @subscribed_client = nil
110
120
  end
111
121
  end
112
122
  end
@@ -119,8 +129,8 @@ module ActionCable
119
129
  return if @thread.nil?
120
130
 
121
131
  when_connected do
122
- send_command("unsubscribe")
123
- @raw_client = nil
132
+ @subscribed_client.unsubscribe
133
+ @subscribed_client = nil
124
134
  end
125
135
  end
126
136
 
@@ -131,13 +141,13 @@ module ActionCable
131
141
  @subscription_lock.synchronize do
132
142
  ensure_listener_running
133
143
  @subscribe_callbacks[channel] << on_success
134
- when_connected { send_command("subscribe", channel) }
144
+ when_connected { @subscribed_client.subscribe(channel) }
135
145
  end
136
146
  end
137
147
 
138
148
  def remove_channel(channel)
139
149
  @subscription_lock.synchronize do
140
- when_connected { send_command("unsubscribe", channel) }
150
+ when_connected { @subscribed_client.unsubscribe(channel) }
141
151
  end
142
152
  end
143
153
 
@@ -150,28 +160,94 @@ module ActionCable
150
160
  @thread ||= Thread.new do
151
161
  Thread.current.abort_on_exception = true
152
162
 
153
- conn = @adapter.redis_connection_for_subscriptions
154
- listen conn
163
+ begin
164
+ conn = @adapter.redis_connection_for_subscriptions
165
+ listen conn
166
+ rescue ConnectionError
167
+ reset
168
+ if retry_connecting?
169
+ when_connected { resubscribe }
170
+ retry
171
+ end
172
+ end
155
173
  end
156
174
  end
157
175
 
158
176
  def when_connected(&block)
159
- if @raw_client
177
+ if @subscribed_client
160
178
  block.call
161
179
  else
162
180
  @when_connected << block
163
181
  end
164
182
  end
165
183
 
166
- def send_command(*command)
167
- @raw_client.write(command)
184
+ def retry_connecting?
185
+ @reconnect_attempt += 1
186
+
187
+ return false if @reconnect_attempt > @reconnect_attempts.size
188
+
189
+ sleep_t = @reconnect_attempts[@reconnect_attempt - 1]
190
+
191
+ sleep(sleep_t) if sleep_t > 0
192
+
193
+ true
194
+ end
195
+
196
+ def resubscribe
197
+ channels = @sync.synchronize do
198
+ @subscribers.keys
199
+ end
200
+ @subscribed_client.subscribe(*channels) unless channels.empty?
201
+ end
202
+
203
+ def reset
204
+ @subscription_lock.synchronize do
205
+ @subscribed_client = nil
206
+ @subscribe_callbacks.clear
207
+ @when_connected.clear
208
+ end
209
+ end
210
+
211
+ if ::Redis::VERSION < "5"
212
+ ConnectionError = ::Redis::ConnectionError
213
+
214
+ class SubscribedClient
215
+ def initialize(raw_client)
216
+ @raw_client = raw_client
217
+ end
218
+
219
+ def subscribe(*channel)
220
+ send_command("subscribe", *channel)
221
+ end
222
+
223
+ def unsubscribe(*channel)
224
+ send_command("unsubscribe", *channel)
225
+ end
168
226
 
169
- very_raw_connection =
170
- @raw_client.connection.instance_variable_defined?(:@connection) &&
171
- @raw_client.connection.instance_variable_get(:@connection)
227
+ private
228
+ def send_command(*command)
229
+ @raw_client.write(command)
230
+
231
+ very_raw_connection =
232
+ @raw_client.connection.instance_variable_defined?(:@connection) &&
233
+ @raw_client.connection.instance_variable_get(:@connection)
234
+
235
+ if very_raw_connection && very_raw_connection.respond_to?(:flush)
236
+ very_raw_connection.flush
237
+ end
238
+ nil
239
+ end
240
+ end
241
+
242
+ def extract_subscribed_client(conn)
243
+ raw_client = conn.respond_to?(:_client) ? conn._client : conn.client
244
+ SubscribedClient.new(raw_client)
245
+ end
246
+ else
247
+ ConnectionError = RedisClient::ConnectionError
172
248
 
173
- if very_raw_connection && very_raw_connection.respond_to?(:flush)
174
- very_raw_connection.flush
249
+ def extract_subscribed_client(conn)
250
+ conn
175
251
  end
176
252
  end
177
253
  end
@@ -7,7 +7,7 @@ module ActionCable
7
7
  # == Test adapter for Action Cable
8
8
  #
9
9
  # The test adapter should be used only in testing. Along with
10
- # <tt>ActionCable::TestHelper</tt> it makes a great tool to test your Rails application.
10
+ # ActionCable::TestHelper it makes a great tool to test your Rails application.
11
11
  #
12
12
  # To use the test adapter set +adapter+ value to +test+ in your +config/cable.yml+ file.
13
13
  #
@@ -45,7 +45,7 @@ module ActionCable
45
45
  def assert_broadcasts(stream, number, &block)
46
46
  if block_given?
47
47
  original_count = broadcasts_size(stream)
48
- assert_nothing_raised(&block)
48
+ _assert_nothing_raised_or_warn("assert_broadcasts", &block)
49
49
  new_count = broadcasts_size(stream)
50
50
  actual_count = new_count - original_count
51
51
  else
@@ -106,7 +106,7 @@ module ActionCable
106
106
  old_messages = new_messages
107
107
  clear_messages(stream)
108
108
 
109
- assert_nothing_raised(&block)
109
+ _assert_nothing_raised_or_warn("assert_broadcast_on", &block)
110
110
  new_messages = broadcasts(stream)
111
111
  clear_messages(stream)
112
112
 
@@ -3,7 +3,7 @@
3
3
  require_relative "gem_version"
4
4
 
5
5
  module ActionCable
6
- # Returns the version of the currently loaded Action Cable as a <tt>Gem::Version</tt>
6
+ # Returns the currently loaded version of Action Cable as a <tt>Gem::Version</tt>.
7
7
  def self.version
8
8
  gem_version
9
9
  end
@@ -10,4 +10,4 @@ Example:
10
10
  creates a Chat channel class, test and JavaScript asset:
11
11
  Channel: app/channels/chat_channel.rb
12
12
  Test: test/channels/chat_channel_test.rb
13
- Assets: app/javascript/channels/chat_channel.js
13
+ Assets: $JAVASCRIPT_PATH/channels/chat_channel.js
@@ -13,39 +13,98 @@ module Rails
13
13
 
14
14
  hook_for :test_framework
15
15
 
16
- def create_channel_file
17
- template "channel.rb", File.join("app/channels", class_path, "#{file_name}_channel.rb")
16
+ def create_channel_files
17
+ create_shared_channel_files
18
+ create_channel_file
18
19
 
19
- if options[:assets]
20
- if behavior == :invoke
21
- template "javascript/index.js", "app/javascript/channels/index.js"
22
- template "javascript/consumer.js", "app/javascript/channels/consumer.js"
20
+ if using_javascript?
21
+ if first_setup_required?
22
+ create_shared_channel_javascript_files
23
+ import_channels_in_javascript_entrypoint
24
+
25
+ if using_importmap?
26
+ pin_javascript_dependencies
27
+ elsif using_node?
28
+ install_javascript_dependencies
29
+ end
23
30
  end
24
31
 
25
- js_template "javascript/channel", File.join("app/javascript/channels", class_path, "#{file_name}_channel")
32
+ create_channel_javascript_file
33
+ import_channel_in_javascript_entrypoint
26
34
  end
27
-
28
- generate_application_cable_files
29
35
  end
30
36
 
31
37
  private
38
+ def create_shared_channel_files
39
+ return if behavior != :invoke
40
+
41
+ copy_file "#{__dir__}/templates/application_cable/channel.rb",
42
+ "app/channels/application_cable/channel.rb"
43
+ copy_file "#{__dir__}/templates/application_cable/connection.rb",
44
+ "app/channels/application_cable/connection.rb"
45
+ end
46
+
47
+ def create_channel_file
48
+ template "channel.rb",
49
+ File.join("app/channels", class_path, "#{file_name}_channel.rb")
50
+ end
51
+
52
+ def create_shared_channel_javascript_files
53
+ template "javascript/index.js", "app/javascript/channels/index.js"
54
+ template "javascript/consumer.js", "app/javascript/channels/consumer.js"
55
+ end
56
+
57
+ def create_channel_javascript_file
58
+ channel_js_path = File.join("app/javascript/channels", class_path, "#{file_name}_channel")
59
+ js_template "javascript/channel", channel_js_path
60
+ gsub_file "#{channel_js_path}.js", /\.\/consumer/, "channels/consumer" unless using_node?
61
+ end
62
+
63
+ def import_channels_in_javascript_entrypoint
64
+ append_to_file "app/javascript/application.js",
65
+ using_node? ? %(import "./channels"\n) : %(import "channels"\n)
66
+ end
67
+
68
+ def import_channel_in_javascript_entrypoint
69
+ append_to_file "app/javascript/channels/index.js",
70
+ using_node? ? %(import "./#{file_name}_channel"\n) : %(import "channels/#{file_name}_channel"\n)
71
+ end
72
+
73
+ def install_javascript_dependencies
74
+ say "Installing JavaScript dependencies", :green
75
+ run "yarn add @rails/actioncable"
76
+ end
77
+
78
+ def pin_javascript_dependencies
79
+ append_to_file "config/importmap.rb", <<-RUBY
80
+ pin "@rails/actioncable", to: "actioncable.esm.js"
81
+ pin_all_from "app/javascript/channels", under: "channels"
82
+ RUBY
83
+ end
84
+
85
+
32
86
  def file_name
33
87
  @_file_name ||= super.sub(/_channel\z/i, "")
34
88
  end
35
89
 
36
- # FIXME: Change these files to symlinks once RubyGems 2.5.0 is required.
37
- def generate_application_cable_files
38
- return if behavior != :invoke
90
+ def first_setup_required?
91
+ !root.join("app/javascript/channels/index.js").exist?
92
+ end
39
93
 
40
- files = [
41
- "application_cable/channel.rb",
42
- "application_cable/connection.rb"
43
- ]
94
+ def using_javascript?
95
+ @using_javascript ||= options[:assets] && root.join("app/javascript").exist?
96
+ end
44
97
 
45
- files.each do |name|
46
- path = File.join("app/channels/", name)
47
- template(name, path) if !File.exist?(path)
48
- end
98
+ def using_node?
99
+ @using_node ||= root.join("package.json").exist?
100
+ end
101
+
102
+ def using_importmap?
103
+ @using_importmap ||= root.join("config/importmap.rb").exist?
104
+ end
105
+
106
+ def root
107
+ @root ||= Pathname(destination_root)
49
108
  end
50
109
  end
51
110
  end
@@ -1,5 +1 @@
1
- // Load all the channels within this directory and all subdirectories.
2
- // Channel files must be named *_channel.js.
3
-
4
- const channels = require.context('.', true, /_channel\.js$/)
5
- channels.keys().forEach(channels)
1
+ // Import all the channels to be used by Action Cable
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actioncable
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.1.7.8
4
+ version: 7.0.8.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pratik Naik
8
8
  - David Heinemeier Hansson
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-06-04 00:00:00.000000000 Z
12
+ date: 2024-10-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -17,28 +17,28 @@ dependencies:
17
17
  requirements:
18
18
  - - '='
19
19
  - !ruby/object:Gem::Version
20
- version: 6.1.7.8
20
+ version: 7.0.8.6
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - '='
26
26
  - !ruby/object:Gem::Version
27
- version: 6.1.7.8
27
+ version: 7.0.8.6
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: actionpack
30
30
  requirement: !ruby/object:Gem::Requirement
31
31
  requirements:
32
32
  - - '='
33
33
  - !ruby/object:Gem::Version
34
- version: 6.1.7.8
34
+ version: 7.0.8.6
35
35
  type: :runtime
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
39
  - - '='
40
40
  - !ruby/object:Gem::Version
41
- version: 6.1.7.8
41
+ version: 7.0.8.6
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: nio4r
44
44
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +80,8 @@ files:
80
80
  - MIT-LICENSE
81
81
  - README.md
82
82
  - app/assets/javascripts/action_cable.js
83
+ - app/assets/javascripts/actioncable.esm.js
84
+ - app/assets/javascripts/actioncable.js
83
85
  - lib/action_cable.rb
84
86
  - lib/action_cable/channel.rb
85
87
  - lib/action_cable/channel/base.rb
@@ -127,8 +129,8 @@ files:
127
129
  - lib/action_cable/version.rb
128
130
  - lib/rails/generators/channel/USAGE
129
131
  - lib/rails/generators/channel/channel_generator.rb
130
- - lib/rails/generators/channel/templates/application_cable/channel.rb.tt
131
- - lib/rails/generators/channel/templates/application_cable/connection.rb.tt
132
+ - lib/rails/generators/channel/templates/application_cable/channel.rb
133
+ - lib/rails/generators/channel/templates/application_cable/connection.rb
132
134
  - lib/rails/generators/channel/templates/channel.rb.tt
133
135
  - lib/rails/generators/channel/templates/javascript/channel.js.tt
134
136
  - lib/rails/generators/channel/templates/javascript/consumer.js.tt
@@ -140,12 +142,12 @@ licenses:
140
142
  - MIT
141
143
  metadata:
142
144
  bug_tracker_uri: https://github.com/rails/rails/issues
143
- changelog_uri: https://github.com/rails/rails/blob/v6.1.7.8/actioncable/CHANGELOG.md
144
- documentation_uri: https://api.rubyonrails.org/v6.1.7.8/
145
+ changelog_uri: https://github.com/rails/rails/blob/v7.0.8.6/actioncable/CHANGELOG.md
146
+ documentation_uri: https://api.rubyonrails.org/v7.0.8.6/
145
147
  mailing_list_uri: https://discuss.rubyonrails.org/c/rubyonrails-talk
146
- source_code_uri: https://github.com/rails/rails/tree/v6.1.7.8/actioncable
148
+ source_code_uri: https://github.com/rails/rails/tree/v7.0.8.6/actioncable
147
149
  rubygems_mfa_required: 'true'
148
- post_install_message:
150
+ post_install_message:
149
151
  rdoc_options: []
150
152
  require_paths:
151
153
  - lib
@@ -153,15 +155,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
153
155
  requirements:
154
156
  - - ">="
155
157
  - !ruby/object:Gem::Version
156
- version: 2.5.0
158
+ version: 2.7.0
157
159
  required_rubygems_version: !ruby/object:Gem::Requirement
158
160
  requirements:
159
161
  - - ">="
160
162
  - !ruby/object:Gem::Version
161
163
  version: '0'
162
164
  requirements: []
163
- rubygems_version: 3.3.27
164
- signing_key:
165
+ rubygems_version: 3.5.16
166
+ signing_key:
165
167
  specification_version: 4
166
168
  summary: WebSocket framework for Rails.
167
169
  test_files: []