solid_cable 4.0.2 → 4.1.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: 7a295176c880ba62299a280e557d841741ad471d57262ba535a8328787bb24a5
4
- data.tar.gz: 18755bf5c482a9445ac9669ae2c341d5adc5654032388d8aa5bafcc21d7ab95b
3
+ metadata.gz: c8c6572058c2cfbbfe229f6bc376f1459611b40005e66396c82b419282535263
4
+ data.tar.gz: 6a480dc962405e9c97ab4efa82e47132e26bbfbc2a159f67f3c484de25655092
5
5
  SHA512:
6
- metadata.gz: d8ed4f55e62d3ced9d59c6024de2809360d2d9bd16e26dcfe0ae0a209e84d034c30857fd30d8aa2ecbd5410755de880b26ac2139a19346eeff3a40f5fce26bff
7
- data.tar.gz: 218429e22a991e5697d3431cc95510a68071eb1b9e03c5bd2107ae668eb26e13ac25e6d59a5d2b4ef898081eaff01fa3fc6f962dbaa699c866bb735b146d1660
6
+ metadata.gz: 35e2ecf675522441a1b4c8c088e63950aa2b7506a8cbdace50fae576e1de8902dbcb5e551587903e66648d1fc47ee8778480517631a37648ee9afcfbb96896a5
7
+ data.tar.gz: addcc08fd66aa27e4aa717b5ce1f1508efba528da6285489ba0676b2b044f37237ce065f8e2f6ab792d21bfc9989e5525550e6bd23da07cb4f0a8a6528af4fe0
data/README.md CHANGED
@@ -88,11 +88,45 @@ The options are:
88
88
  - `trim_batch_size` - the batch size to use when deleting old records (default: `100`)
89
89
  - `reconnect_attempts` - Supports a number of connection attempts or an array of
90
90
  durations to wait between attempts. (Defaults to 1 retry attempt)
91
+ - `encrypt` - whether to encrypt message payloads with Active Record Encryption.
92
+ (Defaults to false)
93
+
94
+ ### Enabling encryption
95
+
96
+ Solid Cable can encrypt stored message payloads with Active Record Encryption. Add
97
+ `encrypt: true` to the Solid Cable environment in `config/cable.yml`:
98
+
99
+ ```yaml
100
+ production:
101
+ adapter: solid_cable
102
+ encrypt: true
103
+ connects_to:
104
+ database:
105
+ writing: cable
106
+ ```
107
+
108
+ Your application must also be [configured to use Active Record Encryption](https://guides.rubyonrails.org/active_record_encryption.html#setup).
109
+ Solid Cable uses the binary MessagePack serializer by default, so your application
110
+ must include the `msgpack` gem.
111
+
112
+ Since encryption context properties contain Ruby objects, they cannot be set in
113
+ `config/cable.yml`. Set them in an initializer instead:
114
+
115
+ ```ruby
116
+ # config/initializers/solid_cable.rb
117
+ SolidCable.configuration.encryption_context_properties = {
118
+ encryptor: ActiveRecord::Encryption::Encryptor.new,
119
+ message_serializer: ActiveRecord::Encryption::MessageSerializer.new
120
+ }
121
+ ```
122
+
123
+ Active Record Encryption does not support encrypted binary columns on PostgreSQL
124
+ with Rails 7. Solid Cable raises during boot for that unsupported combination.
91
125
 
92
126
 
93
127
  ## Trimming
94
128
 
95
- Messages are autotrimmed based upon the `message_retention` setting to determine how long messages are to be kept around. If no `message_retention` is given or parsing fails, it defaults to `1.day`. Messages are trimmed when a messsage is broadcast.
129
+ Messages are autotrimmed based upon the `message_retention` setting to determine how long messages are to be kept around. If no `message_retention` is given or parsing fails, it defaults to `1.day`. For every message written, Solid Cable attempts to trim twice as many expired messages.
96
130
 
97
131
  Autotrimming can negatively impact performance slightly depending on your workload because it is potentially doing a delete on broadcast. If
98
132
  you would prefer, you can disable autotrimming by setting `autotrim: false` and you can manually enqueue the job later, `SolidCable::TrimJob.perform_later`, or run it on a recurring interval out of band.
@@ -2,26 +2,12 @@
2
2
 
3
3
  module SolidCable
4
4
  class TrimJob < ActiveJob::Base
5
- def perform
6
- return unless trim?
7
-
5
+ def perform(trim_batch_size: ::SolidCable.trim_batch_size)
8
6
  ::SolidCable::Message.transaction do
9
7
  ids = ::SolidCable::Message.trimmable.non_blocking_lock.
10
8
  limit(trim_batch_size).pluck(:id)
11
9
  ::SolidCable::Message.where(id: ids).delete_all
12
10
  end
13
11
  end
14
-
15
- private
16
- def trim_batch_size
17
- ::SolidCable.trim_batch_size
18
- end
19
-
20
- def trim?
21
- expires_per_write = (1 / trim_batch_size.to_f) * ::SolidCable.trim_chance
22
-
23
- !::SolidCable.autotrim? ||
24
- rand < (expires_per_write - expires_per_write.floor)
25
- end
26
12
  end
27
13
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidCable
4
+ class Message
5
+ module Encryption
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ if SolidCable.encrypt?
10
+ encrypts :payload, **SolidCable.encryption_context_properties, support_unencrypted_data: true
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -2,12 +2,14 @@
2
2
 
3
3
  module SolidCable
4
4
  class Message < SolidCable::Record
5
+ include Encryption
6
+
5
7
  scope :trimmable, lambda {
6
8
  where(created_at: ...::SolidCable.message_retention.ago)
7
9
  }
8
- scope :broadcastable, lambda { |channels, last_id|
9
- where(channel_hash: channel_hashes_for(channels)).
10
- where(id: (last_id.to_i + 1)..).order(:id)
10
+ scope :broadcastable, lambda { |channel_hashes, last_id|
11
+ select(column_names.excluding("created_at", "updated_at")).
12
+ where(channel_hash: channel_hashes).where(id: (last_id.to_i + 1)..).order(:id)
11
13
  }
12
14
 
13
15
  class << self
@@ -16,8 +18,12 @@ module SolidCable
16
18
  channel_hash: channel_hash_for(channel) })
17
19
  end
18
20
 
19
- def channel_hashes_for(channels)
20
- channels.map { |channel| channel_hash_for(channel) }
21
+ def broadcast_batch(broadcasts)
22
+ created_at = Time.current
23
+ insert_all broadcasts.map { |message|
24
+ { created_at:, channel: message.channel,
25
+ payload: message.payload, channel_hash: channel_hash_for(message.channel) }
26
+ }
21
27
  end
22
28
 
23
29
  # Need to unpack this as a signed integer since Postgresql and SQLite
@@ -20,23 +20,25 @@ module ActionCable
20
20
  end
21
21
 
22
22
  @listener = nil
23
+ @broadcaster = nil
23
24
  end
24
25
 
25
26
  def broadcast(channel, payload)
26
- ::SolidCable::Message.broadcast(channel, payload)
27
-
28
- ::SolidCable::TrimJob.perform_now if ::SolidCable.autotrim?
27
+ broadcaster.broadcast(channel, payload)
29
28
  end
30
29
 
31
- def subscribe(channel, callback, success_callback = nil)
32
- listener.add_subscriber(channel, callback, success_callback)
30
+ def subscribe(channel, subscriber, success_callback = nil)
31
+ listener.add_subscriber(channel, subscriber, success_callback)
33
32
  end
34
33
 
35
- def unsubscribe(channel, callback)
36
- listener.remove_subscriber(channel, callback)
34
+ def unsubscribe(channel, subscriber)
35
+ listener.remove_subscriber(channel, subscriber)
37
36
  end
38
37
 
39
- delegate :shutdown, to: :listener
38
+ def shutdown
39
+ @broadcaster&.shutdown
40
+ @listener&.shutdown
41
+ end
40
42
 
41
43
  private
42
44
  def listener
@@ -45,6 +47,12 @@ module ActionCable
45
47
  end
46
48
  end
47
49
 
50
+ def broadcaster
51
+ @broadcaster || @mutex.synchronize do
52
+ @broadcaster ||= ::SolidCable::BatchedBroadcaster.new
53
+ end
54
+ end
55
+
48
56
  def pubsub_executor
49
57
  @pubsub_executor ||=
50
58
  if respond_to?(:executor, true)
@@ -122,12 +130,12 @@ module ActionCable
122
130
  end
123
131
 
124
132
  def add_channel(channel, on_success)
125
- channels[channel] = last_message_id
133
+ channels[::SolidCable::Message.channel_hash_for(channel)] = last_message_id
126
134
  on_success.call if on_success
127
135
  end
128
136
 
129
137
  def remove_channel(channel)
130
- channels.delete(channel)
138
+ channels.delete(::SolidCable::Message.channel_hash_for(channel))
131
139
  end
132
140
 
133
141
  def invoke_callback(*)
@@ -147,26 +155,27 @@ module ActionCable
147
155
  end
148
156
 
149
157
  def broadcast_messages
150
- current_channels = channels.dup
151
-
152
158
  ::SolidCable::Message.
153
- broadcastable(current_channels.keys, last_id).
154
- each do |message|
159
+ broadcastable(channels.keys, last_id).each do |message|
155
160
  should_broadcast_message = false
156
- channels.compute_if_present(message.channel) do |channel_last_id|
161
+ channels.compute_if_present(message.channel_hash) do |channel_last_id|
157
162
  break if channel_last_id >= message.id
158
163
 
159
164
  should_broadcast_message = true
160
165
  message.id
161
166
  end
162
167
 
163
- broadcast(message.channel, message.payload) if should_broadcast_message
168
+ broadcast(message) if should_broadcast_message
164
169
  self.last_id = message.id
165
170
  end
166
171
 
167
172
  self.reconnect_attempt = 0
168
173
  end
169
174
 
175
+ def broadcast(message)
176
+ super(message.channel, message.payload)
177
+ end
178
+
170
179
  def with_polling_volume
171
180
  if ::SolidCable.silence_polling? && ActiveRecord::Base.logger
172
181
  ActiveRecord::Base.logger.silence { yield }
@@ -15,3 +15,5 @@ production:
15
15
  writing: cable
16
16
  polling_interval: 0.1.seconds
17
17
  message_retention: 1.day
18
+ writer_batch_size: 4
19
+ writer_batch_delay: 0.001.seconds
@@ -4,7 +4,6 @@ ActiveRecord::Schema[7.1].define(version: 1) do
4
4
  t.binary "payload", limit: 536870912, null: false
5
5
  t.datetime "created_at", null: false
6
6
  t.integer "channel_hash", limit: 8, null: false
7
- t.index ["channel"], name: "index_solid_cable_messages_on_channel"
8
7
  t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash"
9
8
  t.index ["created_at"], name: "index_solid_cable_messages_on_created_at"
10
9
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RemoveChannelIndex < ActiveRecord::Migration[7.2]
4
+ def up
5
+ remove_index :solid_cable_messages, :channel, if_exists: true
6
+ end
7
+
8
+ def down
9
+ add_index :solid_cable_messages, :channel, if_not_exists: true
10
+ end
11
+ end
@@ -11,5 +11,7 @@ class SolidCable::UpdateGenerator < Rails::Generators::Base
11
11
  def copy_files
12
12
  migration_template "db/migrate/create_compact_channel.rb",
13
13
  "db/cable_migrate/create_compact_channel.rb"
14
+ migration_template "db/migrate/remove_channel_index.rb",
15
+ "db/cable_migrate/remove_channel_index.rb"
14
16
  end
15
17
  end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+
5
+ module SolidCable
6
+ class BatchedBroadcaster
7
+ include Trimming
8
+
9
+ Stopped = Class.new(StandardError)
10
+ Message = Struct.new(:channel, :payload, keyword_init: true)
11
+
12
+ def initialize(batch_size: SolidCable.writer_batch_size, batch_delay: SolidCable.writer_batch_delay)
13
+ @batch_size = batch_size
14
+ @batch_delay = batch_delay
15
+ @queue = Queue.new
16
+ @background = Concurrent::FixedThreadPool.new(1, max_queue: 100, fallback_policy: :discard)
17
+
18
+ @thread = Thread.new do
19
+ Thread.current.name = "solid_cable_writer"
20
+ Thread.current.abort_on_exception = true
21
+ listen_for_initial_messages
22
+ end
23
+ end
24
+
25
+ def broadcast(channel, payload)
26
+ message = Message.new(channel:, payload:)
27
+
28
+ queue.enq message
29
+ rescue ClosedQueueError
30
+ raise Stopped, "Solid Cable writer has stopped"
31
+ end
32
+
33
+ def shutdown
34
+ queue.close
35
+ thread.join
36
+ background.shutdown
37
+ background.wait_for_termination
38
+ end
39
+
40
+ private
41
+ attr_reader :batch_size, :batch_delay, :queue, :thread, :background
42
+
43
+ def listen_for_initial_messages
44
+ loop do
45
+ message = queue.pop
46
+
47
+ break if message.nil?
48
+
49
+ collect_batch(message)
50
+ end
51
+ end
52
+
53
+ def collect_batch(first_message)
54
+ batch = [ first_message ]
55
+ deadline = monotonic_time + batch_delay
56
+
57
+ drain_queue_into(batch)
58
+ wait_for_messages_until(batch, deadline)
59
+
60
+ flush(batch)
61
+ end
62
+
63
+ def drain_queue_into(batch)
64
+ while batch.size < batch_size && (message = queue.pop(timeout: 0))
65
+ batch << message
66
+ end
67
+ end
68
+
69
+ def wait_for_messages_until(batch, deadline)
70
+ while batch.size < batch_size && (remaining = deadline - monotonic_time).positive?
71
+ message = queue.pop(timeout: remaining)
72
+ break if message.nil?
73
+
74
+ batch << message
75
+ end
76
+ end
77
+
78
+ def flush(batch)
79
+ Rails.application.executor.wrap do
80
+ SolidCable::Message.broadcast_batch(batch)
81
+ track_writes(batch.size) if SolidCable.autotrim?
82
+ end
83
+ rescue StandardError => error
84
+ Rails.error.report(error)
85
+ end
86
+
87
+ def async(&block)
88
+ background << -> do
89
+ Rails.application.executor.wrap(&block)
90
+ rescue Exception => error # rubocop:disable Lint/RescueException
91
+ Rails.error.report(error)
92
+ end
93
+ end
94
+
95
+ def monotonic_time
96
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,104 @@
1
+ module SolidCable
2
+ class Configuration
3
+ def initialize(**options)
4
+ @options = ActiveSupport::InheritableOptions.new(options)
5
+ end
6
+
7
+ attr_writer :connects_to, :silence_polling, :polling_interval,
8
+ :message_retention, :autotrim, :trim_batch_size, :use_skip_locked,
9
+ :reconnect_attempts, :writer_batch_size, :writer_batch_delay,
10
+ :encrypt, :encryption_context_properties
11
+
12
+ def connects_to
13
+ @connects_to ||= options.connects_to.to_h.deep_transform_values(&:to_sym)
14
+ end
15
+
16
+ def silence_polling?
17
+ return @silence_polling if defined?(@silence_polling)
18
+
19
+ @silence_polling = options.silence_polling != false
20
+ end
21
+
22
+ def polling_interval
23
+ @polling_interval ||=
24
+ parse_duration(options.polling_interval, default: 0.1.seconds)
25
+ end
26
+
27
+ def message_retention
28
+ @message_retention ||= parse_duration(options.message_retention, default: 1.day)
29
+ end
30
+
31
+ def autotrim?
32
+ return @autotrim if defined?(@autotrim)
33
+
34
+ @autotrim = options.autotrim != false
35
+ end
36
+
37
+ def trim_batch_size
38
+ @trim_batch_size ||=
39
+ if (size = options.trim_batch_size.to_i) < 2
40
+ 100
41
+ else
42
+ size
43
+ end
44
+ end
45
+
46
+ def use_skip_locked
47
+ return @use_skip_locked if defined?(@use_skip_locked)
48
+
49
+ @use_skip_locked = options.use_skip_locked != false
50
+ end
51
+
52
+ def reconnect_attempts
53
+ @reconnect_attempts ||= begin
54
+ attempts = options[:reconnect_attempts] || 1
55
+ attempts = Array.new(attempts, 0) if attempts.is_a?(Integer)
56
+ attempts
57
+ end
58
+ end
59
+
60
+ def writer_batch_size
61
+ @writer_batch_size ||= [ (options.writer_batch_size || 4).to_i, 1 ].max
62
+ end
63
+
64
+ def writer_batch_delay
65
+ @writer_batch_delay ||=
66
+ [ parse_duration(options.writer_batch_delay, default: 0.001.seconds), 0 ].max
67
+ end
68
+
69
+ def encrypt?
70
+ return @encrypt if defined?(@encrypt)
71
+
72
+ @encrypt = options.encrypt.present?
73
+ end
74
+
75
+ def encryption_context_properties
76
+ return @encryption_context_properties if defined?(@encryption_context_properties)
77
+
78
+ @encryption_context_properties = options.encryption_context_properties&.deep_symbolize_keys
79
+ @encryption_context_properties ||= default_encryption_context_properties if encrypt?
80
+ end
81
+
82
+ private
83
+ attr_reader :options
84
+
85
+ def default_encryption_context_properties
86
+ require "active_record/encryption/message_pack_message_serializer"
87
+
88
+ {
89
+ encryptor: ActiveRecord::Encryption::Encryptor.new(compress: true),
90
+ # Binary column only serializer that is 40% more efficient than the default MessageSerializer
91
+ message_serializer: ActiveRecord::Encryption::MessagePackMessageSerializer.new
92
+ }
93
+ end
94
+
95
+ def parse_duration(duration, default:)
96
+ if duration.present?
97
+ *amount, units = duration.to_s.split(".")
98
+ amount.join(".").to_f.public_send(units)
99
+ else
100
+ default
101
+ end
102
+ end
103
+ end
104
+ end
@@ -3,5 +3,13 @@
3
3
  module SolidCable
4
4
  class Engine < ::Rails::Engine
5
5
  isolate_namespace SolidCable
6
+
7
+ config.after_initialize do
8
+ if SolidCable.encrypt? && Record.lease_connection.adapter_name == "PostgreSQL" && Rails::VERSION::MAJOR == 7
9
+ raise \
10
+ "Cannot enable encryption for Solid Cable: in Rails 7, Active Record Encryption does not support " \
11
+ "encrypting binary columns on PostgreSQL"
12
+ end
13
+ end
6
14
  end
7
15
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidCable
4
+ module Trimming
5
+ # For every write that we do, we attempt to delete TRIM_MULTIPLIER times as
6
+ # many records. This ensures there is downward pressure on the message count
7
+ # while there is old data to delete.
8
+ TRIM_MULTIPLIER = 2
9
+
10
+ private
11
+ def track_writes(count)
12
+ trim_batches(count).times { async { TrimJob.perform_now } }
13
+ end
14
+
15
+ def trim_batches(count)
16
+ trims_per_write = (1 / SolidCable.trim_batch_size.to_f) * TRIM_MULTIPLIER
17
+ batches = (count * trims_per_write).floor
18
+ overflow_batch_chance = count * trims_per_write - batches
19
+ batches += 1 if rand < overflow_batch_chance
20
+ batches
21
+ end
22
+ end
23
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidCable
4
- VERSION = "4.0.2"
4
+ VERSION = "4.1.0"
5
5
  end
data/lib/solid_cable.rb CHANGED
@@ -2,70 +2,29 @@
2
2
 
3
3
  require "solid_cable/version"
4
4
  require "solid_cable/engine"
5
+ require "solid_cable/configuration"
6
+ require "solid_cable/trimming"
7
+ require "solid_cable/batched_broadcaster"
5
8
  require "action_cable/subscription_adapter/solid_cable"
6
9
 
7
10
  module SolidCable
8
11
  class << self
9
- def connects_to
10
- cable_config.connects_to.to_h.deep_transform_values(&:to_sym)
11
- end
12
-
13
- def silence_polling?
14
- cable_config.silence_polling != false
15
- end
16
-
17
- def polling_interval
18
- parse_duration(cable_config.polling_interval, default: 0.1.seconds)
19
- end
20
-
21
- def message_retention
22
- parse_duration(cable_config.message_retention, default: 1.day)
23
- end
12
+ delegate :connects_to, :silence_polling?, :polling_interval,
13
+ :message_retention, :autotrim?, :trim_batch_size, :use_skip_locked,
14
+ :reconnect_attempts, :writer_batch_size, :writer_batch_delay,
15
+ :encrypt?, :encryption_context_properties,
16
+ to: :configuration
24
17
 
25
- def autotrim?
26
- cable_config.autotrim != false
18
+ def configuration
19
+ @configuration ||= Configuration.new(**Rails.application.config_for("cable"))
27
20
  end
28
21
 
29
- def trim_batch_size
30
- if (size = cable_config.trim_batch_size.to_i) < 2
31
- 100
32
- else
33
- size
34
- end
22
+ def configure(**options)
23
+ @configuration = Configuration.new(**options)
35
24
  end
36
25
 
37
- def use_skip_locked
38
- cable_config.use_skip_locked != false
26
+ def reset_configuration!
27
+ @configuration = nil
39
28
  end
40
-
41
- # For every write that we do, we attempt to delete trim_chance times as
42
- # many records. This ensures there is downward pressure on the cache size
43
- # while there is valid data to delete. Read this as 'every time the trim job
44
- # runs theres a trim_multiplier chance this trims'. Adjust number to make it
45
- # more or less likely to trim. Only works like this if trim_batch_size is
46
- # 100
47
- def trim_chance
48
- 2
49
- end
50
-
51
- def reconnect_attempts
52
- attempts = cable_config.fetch(:reconnect_attempts, 1)
53
- attempts = Array.new(attempts, 0) if attempts.is_a?(Integer)
54
- attempts
55
- end
56
-
57
- private
58
- def cable_config
59
- Rails.application.config_for("cable")
60
- end
61
-
62
- def parse_duration(duration, default:)
63
- if duration.present?
64
- *amount, units = duration.to_s.split(".")
65
- amount.join(".").to_f.public_send(units)
66
- else
67
- default
68
- end
69
- end
70
29
  end
71
30
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_cable
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.2
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Pezza
@@ -65,6 +65,20 @@ dependencies:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
67
  version: '7.2'
68
+ - !ruby/object:Gem::Dependency
69
+ name: msgpack
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
68
82
  - !ruby/object:Gem::Dependency
69
83
  name: minitest
70
84
  requirement: !ruby/object:Gem::Requirement
@@ -91,6 +105,7 @@ files:
91
105
  - Rakefile
92
106
  - app/jobs/solid_cable/trim_job.rb
93
107
  - app/models/solid_cable/message.rb
108
+ - app/models/solid_cable/message/encryption.rb
94
109
  - app/models/solid_cable/record.rb
95
110
  - lib/action_cable/subscription_adapter/solid_cable.rb
96
111
  - lib/generators/solid_cable/install/USAGE
@@ -99,9 +114,13 @@ files:
99
114
  - lib/generators/solid_cable/install/templates/db/cable_schema.rb
100
115
  - lib/generators/solid_cable/update/USAGE
101
116
  - lib/generators/solid_cable/update/templates/db/migrate/create_compact_channel.rb
117
+ - lib/generators/solid_cable/update/templates/db/migrate/remove_channel_index.rb
102
118
  - lib/generators/solid_cable/update/update_generator.rb
103
119
  - lib/solid_cable.rb
120
+ - lib/solid_cable/batched_broadcaster.rb
121
+ - lib/solid_cable/configuration.rb
104
122
  - lib/solid_cable/engine.rb
123
+ - lib/solid_cable/trimming.rb
105
124
  - lib/solid_cable/version.rb
106
125
  - lib/tasks/solid_cable_tasks.rake
107
126
  homepage: https://github.com/rails/solid_cable
@@ -125,7 +144,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
125
144
  - !ruby/object:Gem::Version
126
145
  version: '0'
127
146
  requirements: []
128
- rubygems_version: 4.0.15
147
+ rubygems_version: 4.0.20
129
148
  specification_version: 4
130
149
  summary: Database-backed Action Cable backend.
131
150
  test_files: []