actioncable-next 0.1.2 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b86ee72b0095c7a8ce99b0d354231b2ac29145e07a1482387fa9664f1c9a08b8
4
- data.tar.gz: e72beed883e8789189fa6811d6fb70cdc4e9efbec2ad9b259648c8575196aca3
3
+ metadata.gz: f5831ed0f565c3bc58899a26ec3097bf47ea7cb518f380e0db05a8bad6fcc5dd
4
+ data.tar.gz: 92aae3e36883530c9b51ee7acdb0e7df231aed27d60b5f8a4d31426c7158822e
5
5
  SHA512:
6
- metadata.gz: 955b568fb0d4e01d81ad6871292515c1a3d742eef03067c6322d68b7c6908eeac12947aaef91cb9d4fcbccf2e3790ff84c797ad30e462db15179b04ee6a2d8a3
7
- data.tar.gz: 3cc6151d155e3719b008bad22ecf4fbf0a401a5858a7ef5bd75a941debca5b26ac9d06cb30d781929d6da10399c9f534d06aed6ac7afd2ea7a96490066dc6891
6
+ metadata.gz: ef8e96d02d595e9004acc936acc8c116d1ac846664dbdf3455b0cfb8f872ba3dbbecfd3dfb3772a53b796505c02a592947bf838690d9c5f0b2c844a40e70e7a4
7
+ data.tar.gz: bf36b4a97cd76f02d4162a70d27751ae15333d5c1c23654199bd59214dc061b0b779aa9dd28c73ca080f075fc5bc8972db86d6e98ce137c4b78f330bd3e5540c
data/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## main
4
4
 
5
+ ## 0.2.0
6
+
7
+ - Performance: store connections in a Hash, not an Array.
8
+
9
+ - Add `Connection#broadcast` as an interface for broadcasting from channels.
10
+
11
+ - Add `config.fastlane_broadcasts_enabled` to opt-in for optimized broadcasts (no double JSON encoding).
12
+
5
13
  ## 0.1.2
6
14
 
7
15
  - Added a hack to prevent third-party extensions from changing the methods visibility.
data/README.md CHANGED
@@ -4,6 +4,8 @@ This gem provides the functionality of the _server adapterization_ PR: [rails/ra
4
4
 
5
5
  See the PR description for more information on the purpose of this refactoring.
6
6
 
7
+ There are also some additional improvements added on top of the PR (see below).
8
+
7
9
  ## Usage
8
10
 
9
11
  Add this line to your application's Gemfile **before Rails or Action Cable**:
@@ -26,3 +28,23 @@ by adding the following line to your `spec/rails_helper.rb`:
26
28
  require "rspec/rails"
27
29
  require "action_cable/next/rspec"
28
30
  ```
31
+
32
+ ### New features
33
+
34
+ #### `ActionCable.server.config.fastlane_broadcasts_enabled = true`
35
+
36
+ This option allows you to enable the _fast lane_ for broadcasting messages. When enabled, messages sent via `ActionCable.server.broadcast` are only decoded-encoded from/to JSON once per channel identifier and not for every connected client. This significantly reduces the broadcasting latency (up to 2x faster).
37
+
38
+ #### `Connection#broadcast`
39
+
40
+ You can now broadcast message from connection or channel instances using the connection `#broadcast` method. For example:
41
+
42
+ ```ruby
43
+ class TestChannel < ActionCable::Channel::Base
44
+ def notify_all(data)
45
+ connection.broadcast("testing-#{test_id}", data)
46
+ end
47
+ end
48
+ ```
49
+
50
+ The purpose of this interface is to avoid direct usage of `server` or `ActionCable.server` in channels code.
@@ -157,10 +157,21 @@ module ActionCable
157
157
  # Always wrap the outermost handler to invoke the user handler on the worker
158
158
  # pool rather than blocking the event loop.
159
159
  def worker_pool_stream_handler(broadcasting, user_handler, coder: nil)
160
+ if !user_handler && !coder && connection.config.fastlane_broadcasts_enabled
161
+ return opt_worker_pool_stream_handler(broadcasting)
162
+ end
163
+
160
164
  handler = stream_handler(broadcasting, user_handler, coder: coder)
161
165
 
162
166
  -> message do
163
- connection.perform_work handler, :call, message
167
+ connection.perform_work handler, :call, message.data
168
+ end
169
+ end
170
+
171
+ # Optimized stream handler that avoids double JSON encoding/decoding on broadcast
172
+ def opt_worker_pool_stream_handler(broadcasting)
173
+ -> (message) do
174
+ connection.raw_transmit message.encoded_for(@identifier)
164
175
  end
165
176
  end
166
177
 
@@ -62,7 +62,7 @@ module ActionCable
62
62
  attr_reader :subscriptions, :logger
63
63
  private attr_reader :server, :socket
64
64
 
65
- delegate :pubsub, :executor, :config, to: :server
65
+ delegate :pubsub, :executor, :config, :broadcast, to: :server
66
66
  delegate :env, :request, :protocol, :perform_work, to: :socket, allow_nil: true
67
67
 
68
68
  def initialize(server, socket)
@@ -117,6 +117,10 @@ module ActionCable
117
117
  socket.transmit(data)
118
118
  end
119
119
 
120
+ def raw_transmit(data) # :nodoc:
121
+ socket.raw_transmit(data)
122
+ end
123
+
120
124
  # Close the connection.
121
125
  def close(reason: nil, reconnect: true)
122
126
  transmit(
@@ -18,7 +18,7 @@ module ActionCable
18
18
 
19
19
  def subscribe_to_internal_channel
20
20
  if connection_identifier.present?
21
- callback = -> (message) { process_internal_message ActiveSupport::JSON.decode(message) }
21
+ callback = -> (message) { process_internal_message ActiveSupport::JSON.decode(message.data) }
22
22
  @_internal_subscriptions ||= []
23
23
  @_internal_subscriptions << [ internal_channel, callback ]
24
24
 
@@ -19,6 +19,7 @@ module ActionCable
19
19
  attr_accessor :precompile_assets
20
20
  attr_accessor :health_check_path, :health_check_application
21
21
  attr_writer :pubsub_adapter
22
+ attr_accessor :fastlane_broadcasts_enabled
22
23
 
23
24
  def initialize
24
25
  @log_tags = []
@@ -31,6 +32,8 @@ module ActionCable
31
32
  @allow_same_origin_as_host = true
32
33
  @filter_parameters = []
33
34
 
35
+ @fastlane_broadcasts_enabled = false
36
+
34
37
  @health_check_application = ->(env) {
35
38
  [200, { Rack::CONTENT_TYPE => "text/html", "date" => Time.now.httpdate }, []]
36
39
  }
@@ -13,16 +13,18 @@ module ActionCable
13
13
  module Connections # :nodoc:
14
14
  BEAT_INTERVAL = 3
15
15
 
16
- def connections
17
- @connections ||= []
16
+ def connections_map
17
+ @connections_map ||= {}
18
18
  end
19
19
 
20
+ def connections = connections_map.values
21
+
20
22
  def add_connection(connection)
21
- connections << connection
23
+ connections_map[connection.object_id] = connection
22
24
  end
23
25
 
24
26
  def remove_connection(connection)
25
- connections.delete connection
27
+ connections_map.delete connection.object_id
26
28
  end
27
29
 
28
30
  # WebSocket connection implementations differ on when they'll mark a connection
@@ -32,12 +34,12 @@ module ActionCable
32
34
  # disconnect.
33
35
  def setup_heartbeat_timer
34
36
  @heartbeat_timer ||= executor.timer(BEAT_INTERVAL) do
35
- executor.post { connections.each(&:beat) }
37
+ executor.post { connections_map.each_value(&:beat) }
36
38
  end
37
39
  end
38
40
 
39
41
  def open_connections_statistics
40
- connections.map(&:statistics)
42
+ connections_map.each_value.map(&:statistics)
41
43
  end
42
44
  end
43
45
  end
@@ -47,6 +47,12 @@ module ActionCable
47
47
  websocket.transmit encode(cable_message)
48
48
  end
49
49
 
50
+ def raw_transmit(message)
51
+ return unless websocket.alive?
52
+
53
+ websocket.transmit message
54
+ end
55
+
50
56
  # Close the WebSocket connection.
51
57
  def close(...)
52
58
  websocket.close(...) if websocket.alive?
@@ -5,6 +5,19 @@
5
5
  module ActionCable
6
6
  module SubscriptionAdapter
7
7
  class SubscriberMap
8
+ class Message < Struct.new(:data)
9
+ def initialize(...)
10
+ super
11
+ @cache = Concurrent::Map.new
12
+ end
13
+
14
+ def encoded_for(identifier)
15
+ @cache.compute_if_absent(identifier) do
16
+ ActiveSupport::JSON.encode({ identifier: identifier, message: ActiveSupport::JSON.decode(data) })
17
+ end
18
+ end
19
+ end
20
+
8
21
  def initialize
9
22
  @subscribers = Hash.new { |h, k| h[k] = [] }
10
23
  @sync = Mutex.new
@@ -41,8 +54,10 @@ module ActionCable
41
54
  @subscribers[channel].dup
42
55
  end
43
56
 
57
+ msg = Message.new(message)
58
+
44
59
  list.each do |subscriber|
45
- invoke_callback(subscriber, message)
60
+ invoke_callback(subscriber, msg)
46
61
  end
47
62
  end
48
63
 
data/lib/action_cable.rb CHANGED
@@ -23,6 +23,7 @@
23
23
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
24
  #++
25
25
 
26
+ require "logger"
26
27
  require "active_support"
27
28
  require "active_support/rails"
28
29
  require "zeitwerk"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionCableNext
4
- VERSION = "0.1.2"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actioncable-next
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pratik Naik
8
8
  - David Heinemeier Hansson
9
9
  - Vladimir Dementyev
10
- autorequire:
10
+ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2025-01-10 00:00:00.000000000 Z
13
+ date: 2025-02-04 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
@@ -167,10 +167,10 @@ licenses:
167
167
  - MIT
168
168
  metadata:
169
169
  bug_tracker_uri: https://github.com/anycable/actioncable-next
170
- changelog_uri: https://github.com/anycable/actioncable-next/blob/v0.1.2/CHANGELOG.md
170
+ changelog_uri: https://github.com/anycable/actioncable-next/blob/v0.2.0/CHANGELOG.md
171
171
  source_code_uri: https://github.com/anycable/actioncable-next
172
172
  rubygems_mfa_required: 'true'
173
- post_install_message:
173
+ post_install_message:
174
174
  rdoc_options: []
175
175
  require_paths:
176
176
  - lib
@@ -186,7 +186,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
186
186
  version: '0'
187
187
  requirements: []
188
188
  rubygems_version: 3.5.22
189
- signing_key:
189
+ signing_key:
190
190
  specification_version: 4
191
191
  summary: Next-gen version of Action Cable
192
192
  test_files: []