cable_ready 4.5.0 → 5.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2 -376
  3. data/Gemfile +4 -1
  4. data/Gemfile.lock +146 -144
  5. data/README.md +54 -20
  6. data/Rakefile +8 -8
  7. data/app/assets/javascripts/cable_ready.js +1269 -0
  8. data/app/assets/javascripts/cable_ready.umd.js +1190 -0
  9. data/app/channels/cable_ready/stream.rb +14 -0
  10. data/app/helpers/cable_ready/view_helper.rb +58 -0
  11. data/app/jobs/cable_ready/broadcast_job.rb +15 -0
  12. data/app/models/concerns/cable_ready/updatable/collection_updatable_callbacks.rb +21 -0
  13. data/app/models/concerns/cable_ready/updatable/collections_registry.rb +59 -0
  14. data/app/models/concerns/cable_ready/updatable/memory_cache_debounce_adapter.rb +24 -0
  15. data/app/models/concerns/cable_ready/updatable/model_updatable_callbacks.rb +33 -0
  16. data/app/models/concerns/cable_ready/updatable.rb +211 -0
  17. data/app/models/concerns/extend_has_many.rb +15 -0
  18. data/bin/standardize +1 -1
  19. data/cable_ready.gemspec +20 -6
  20. data/lib/cable_ready/broadcaster.rb +4 -3
  21. data/lib/cable_ready/cable_car.rb +19 -0
  22. data/lib/cable_ready/channel.rb +29 -31
  23. data/lib/cable_ready/channels.rb +4 -5
  24. data/lib/cable_ready/compoundable.rb +11 -0
  25. data/lib/cable_ready/config.rb +28 -1
  26. data/lib/cable_ready/engine.rb +59 -0
  27. data/lib/cable_ready/identifiable.rb +48 -0
  28. data/lib/cable_ready/importmap.rb +4 -0
  29. data/lib/cable_ready/installer.rb +224 -0
  30. data/lib/cable_ready/operation_builder.rb +80 -0
  31. data/lib/cable_ready/sanity_checker.rb +63 -0
  32. data/lib/cable_ready/stream_identifier.rb +13 -0
  33. data/lib/cable_ready/version.rb +1 -1
  34. data/lib/cable_ready.rb +23 -10
  35. data/lib/cable_ready_helper.rb +13 -0
  36. data/lib/generators/cable_ready/channel_generator.rb +110 -0
  37. data/lib/generators/cable_ready/templates/app/javascript/channels/consumer.js.tt +6 -0
  38. data/lib/generators/cable_ready/templates/app/javascript/channels/index.js.esbuild.tt +4 -0
  39. data/lib/generators/cable_ready/templates/app/javascript/channels/index.js.importmap.tt +2 -0
  40. data/lib/generators/cable_ready/templates/app/javascript/channels/index.js.shakapacker.tt +5 -0
  41. data/lib/generators/cable_ready/templates/app/javascript/channels/index.js.vite.tt +1 -0
  42. data/lib/generators/cable_ready/templates/app/javascript/channels/index.js.webpacker.tt +5 -0
  43. data/lib/generators/cable_ready/templates/app/javascript/config/cable_ready.js.tt +4 -0
  44. data/lib/generators/cable_ready/templates/app/javascript/config/index.js.tt +1 -0
  45. data/lib/generators/cable_ready/templates/app/javascript/config/mrujs.js.tt +9 -0
  46. data/lib/generators/cable_ready/templates/app/javascript/controllers/%file_name%_controller.js.tt +38 -0
  47. data/lib/generators/cable_ready/templates/app/javascript/controllers/application.js.tt +11 -0
  48. data/lib/generators/cable_ready/templates/app/javascript/controllers/index.js.esbuild.tt +7 -0
  49. data/lib/generators/cable_ready/templates/app/javascript/controllers/index.js.importmap.tt +5 -0
  50. data/lib/generators/cable_ready/templates/app/javascript/controllers/index.js.shakapacker.tt +5 -0
  51. data/lib/generators/cable_ready/templates/app/javascript/controllers/index.js.vite.tt +5 -0
  52. data/lib/generators/cable_ready/templates/app/javascript/controllers/index.js.webpacker.tt +5 -0
  53. data/lib/generators/cable_ready/templates/config/initializers/cable_ready.rb +27 -0
  54. data/lib/generators/cable_ready/templates/esbuild.config.mjs.tt +94 -0
  55. data/lib/install/action_cable.rb +144 -0
  56. data/lib/install/broadcaster.rb +109 -0
  57. data/lib/install/bundle.rb +54 -0
  58. data/lib/install/compression.rb +51 -0
  59. data/lib/install/config.rb +39 -0
  60. data/lib/install/development.rb +34 -0
  61. data/lib/install/esbuild.rb +101 -0
  62. data/lib/install/importmap.rb +96 -0
  63. data/lib/install/initializers.rb +15 -0
  64. data/lib/install/mrujs.rb +121 -0
  65. data/lib/install/npm_packages.rb +13 -0
  66. data/lib/install/shakapacker.rb +65 -0
  67. data/lib/install/spring.rb +54 -0
  68. data/lib/install/updatable.rb +34 -0
  69. data/lib/install/vite.rb +66 -0
  70. data/lib/install/webpacker.rb +93 -0
  71. data/lib/install/yarn.rb +56 -0
  72. data/lib/tasks/cable_ready/cable_ready.rake +247 -0
  73. data/package.json +42 -13
  74. data/rollup.config.mjs +57 -0
  75. data/web-test-runner.config.mjs +12 -0
  76. data/yarn.lock +3252 -327
  77. metadata +138 -9
  78. data/tags +0 -62
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CableReady
4
+ if defined?(ActionCable)
5
+ class Stream < ActionCable::Channel::Base
6
+ include CableReady::StreamIdentifier
7
+
8
+ def subscribed
9
+ locator = verified_stream_identifier(params[:identifier])
10
+ locator.present? ? stream_from(locator) : reject
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CableReady
4
+ module ViewHelper
5
+ include CableReady::Compoundable
6
+ include CableReady::StreamIdentifier
7
+
8
+ def stream_from(...)
9
+ warn "DEPRECATED: please use `cable_ready_stream_from` instead. The `stream_from` view helper will be removed from a future version of CableReady 5"
10
+
11
+ cable_ready_stream_from(...)
12
+ end
13
+
14
+ def updates_for(...)
15
+ warn "DEPRECATED: please use `cable_ready_updates_for` instead. The `updates_for` view helper will be removed from a future version of CableReady 5"
16
+
17
+ cable_ready_updates_for(...)
18
+ end
19
+
20
+ def updates_for_if(...)
21
+ warn "DEPRECATED: please use `cable_ready_updates_for_if` instead. The `updates_for_if` view helper will be removed from a future version of CableReady 5"
22
+
23
+ cable_ready_updates_for_if(...)
24
+ end
25
+
26
+ def cable_ready_stream_from(*keys, html_options: {})
27
+ tag.cable_ready_stream_from(**build_options(*keys, html_options))
28
+ end
29
+
30
+ def cable_ready_updates_for(*keys, url: nil, debounce: nil, only: nil, ignore_inner_updates: false, html_options: {}, &block)
31
+ options = build_options(*keys, html_options)
32
+ options[:url] = url if url
33
+ options[:debounce] = debounce if debounce
34
+ options[:only] = only if only
35
+ options[:"ignore-inner-updates"] = "" if ignore_inner_updates
36
+ tag.cable_ready_updates_for(**options) { capture(&block) }
37
+ end
38
+
39
+ def cable_ready_updates_for_if(condition, *keys, **options, &block)
40
+ if condition
41
+ cable_ready_updates_for(*keys, **options, &block)
42
+ else
43
+ capture(&block)
44
+ end
45
+ end
46
+
47
+ def cable_car
48
+ CableReady::CableCar.instance
49
+ end
50
+
51
+ private
52
+
53
+ def build_options(*keys, html_options)
54
+ keys.select!(&:itself)
55
+ {identifier: signed_stream_identifier(compound(keys))}.merge(html_options)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ if defined?(ActiveJob::Base)
4
+ class CableReady::BroadcastJob < ActiveJob::Base
5
+ include CableReady::Broadcaster
6
+
7
+ def perform(identifier:, operations:, model: nil)
8
+ if model.present?
9
+ cable_ready[identifier.safe_constantize].apply!(operations).broadcast_to(model)
10
+ else
11
+ cable_ready[identifier].apply!(operations).broadcast
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CableReady
4
+ module Updatable
5
+ class CollectionUpdatableCallbacks
6
+ def initialize(operation)
7
+ @operation = operation
8
+ end
9
+
10
+ def after_commit(model)
11
+ update_collections(model)
12
+ end
13
+
14
+ private
15
+
16
+ def update_collections(model)
17
+ model.class.cable_ready_collections.broadcast_for!(model, @operation)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CableReady
4
+ module Updatable
5
+ Collection = Struct.new(
6
+ :klass,
7
+ :name,
8
+ :reflection,
9
+ :options,
10
+ :foreign_key,
11
+ :inverse_association,
12
+ :through_association,
13
+ :debounce_time,
14
+ keyword_init: true
15
+ )
16
+
17
+ class CollectionsRegistry
18
+ def initialize
19
+ @registered_collections = []
20
+ end
21
+
22
+ def register(collection)
23
+ @registered_collections << collection
24
+ end
25
+
26
+ def broadcast_for!(model, operation)
27
+ @registered_collections.select { |c| c.options[:on].include?(operation) }
28
+ .each do |collection|
29
+ resource = find_resource_for_update(collection, model)
30
+ next if resource.nil?
31
+
32
+ collection.klass.cable_ready_update_collection(resource, collection.name, model, debounce: collection.debounce_time) if collection.options[:if].call(resource)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def find_resource_for_update(collection, model)
39
+ collection.reflection ||= collection.klass.reflect_on_association(collection.name)
40
+
41
+ collection.foreign_key ||= collection.reflection&.foreign_key
42
+
43
+ # lazy load and store through and inverse associations
44
+ if collection.reflection&.through_reflection?
45
+ collection.inverse_association ||= collection.reflection&.through_reflection&.inverse_of&.name&.to_s
46
+ collection.through_association = collection.reflection&.through_reflection&.name&.to_s&.singularize
47
+ else
48
+ collection.inverse_association ||= collection.reflection&.inverse_of&.name&.to_s
49
+ end
50
+
51
+ raise ArgumentError, "Could not find inverse_of for #{collection.name}" unless collection.inverse_association
52
+
53
+ resource = model
54
+ resource = resource.send(collection.through_association.underscore) if collection.through_association
55
+ resource.send(collection.inverse_association.underscore)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CableReady
4
+ module Updatable
5
+ class MemoryCacheDebounceAdapter
6
+ include Singleton
7
+
8
+ delegate_missing_to :@store
9
+
10
+ def initialize
11
+ super
12
+ @store = ActiveSupport::Cache::MemoryStore.new(expires_in: 5.minutes, size: 8.megabytes)
13
+ end
14
+
15
+ def []=(key, value)
16
+ @store.write(key, value)
17
+ end
18
+
19
+ def [](key)
20
+ @store.read(key)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CableReady
4
+ module Updatable
5
+ class ModelUpdatableCallbacks
6
+ def initialize(operation, enabled_operations = %i[create update destroy], debounce: CableReady.config.updatable_debounce_time)
7
+ @operation = operation
8
+ @enabled_operations = enabled_operations
9
+ @debounce = debounce
10
+ end
11
+
12
+ def after_commit(model)
13
+ return unless @enabled_operations.include?(@operation)
14
+
15
+ send("broadcast_#{@operation}", model)
16
+ end
17
+
18
+ private
19
+
20
+ def broadcast_create(model)
21
+ model.class.send(:broadcast_updates, model.class, {debounce: @debounce})
22
+ end
23
+ alias_method :broadcast_destroy, :broadcast_create
24
+
25
+ def broadcast_update(model)
26
+ changeset = model.respond_to?(:previous_changes) ? {changed: model.previous_changes.keys} : {}
27
+ options = changeset.merge({debounce: @debounce})
28
+ model.class.send(:broadcast_updates, model.class, options.dup)
29
+ model.class.send(:broadcast_updates, model.to_global_id, options)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module CableReady
6
+ module Updatable
7
+ extend ::ActiveSupport::Concern
8
+
9
+ mattr_accessor :debounce_adapter, default: ::CableReady::Updatable::MemoryCacheDebounceAdapter.instance
10
+
11
+ included do |base|
12
+ if defined?(ActiveRecord) && base < ActiveRecord::Base
13
+ include ExtendHasMany
14
+
15
+ after_commit CollectionUpdatableCallbacks.new(:create), on: :create
16
+ after_commit CollectionUpdatableCallbacks.new(:update), on: :update
17
+ after_commit CollectionUpdatableCallbacks.new(:destroy), on: :destroy
18
+
19
+ def self.enable_updates(...)
20
+ warn "DEPRECATED: please use `enable_cable_ready_updates` instead. The `enable_updates` class method will be removed from a future version of CableReady 5"
21
+
22
+ enable_cable_ready_updates(...)
23
+ end
24
+
25
+ def self.skip_updates(...)
26
+ warn "DEPRECATED: please use `skip_cable_ready_updates` instead. The `skip_updates` class method will be removed from a future version of CableReady 5"
27
+
28
+ skip_cable_ready_updates(...)
29
+ end
30
+
31
+ def self.enable_cable_ready_updates(*options)
32
+ options = options.extract_options!
33
+ options = {
34
+ on: [:create, :update, :destroy],
35
+ if: -> { true },
36
+ debounce: CableReady.config.updatable_debounce_time
37
+ }.merge(options)
38
+
39
+ enabled_operations = Array(options[:on])
40
+
41
+ after_commit(ModelUpdatableCallbacks.new(:create, enabled_operations, debounce: options[:debounce]), {on: :create, if: options[:if]})
42
+ after_commit(ModelUpdatableCallbacks.new(:update, enabled_operations, debounce: options[:debounce]), {on: :update, if: options[:if]})
43
+ after_commit(ModelUpdatableCallbacks.new(:destroy, enabled_operations, debounce: options[:debounce]), {on: :destroy, if: options[:if]})
44
+ end
45
+
46
+ def self.skip_cable_ready_updates
47
+ skip_updates_classes.push(self)
48
+ yield
49
+ ensure
50
+ skip_updates_classes.pop
51
+ end
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ module ClassMethods
58
+ include Compoundable
59
+
60
+ def has_many(name, scope = nil, **options, &extension)
61
+ option = if options.has_key?(:enable_updates)
62
+ warn "DEPRECATED: please use `enable_cable_ready_updates` instead. The `enable_updates` option will be removed from a future version of CableReady 5"
63
+ options.delete(:enable_updates)
64
+ else
65
+ options.delete(:enable_cable_ready_updates)
66
+ end
67
+
68
+ descendants = options.delete(:descendants)
69
+ debounce_time = options.delete(:debounce)
70
+
71
+ broadcast = option.present?
72
+ result = super
73
+ enrich_association_with_updates(name, option, descendants, debounce: debounce_time) if broadcast
74
+ result
75
+ end
76
+
77
+ def has_one(name, scope = nil, **options, &extension)
78
+ option = if options.has_key?(:enable_updates)
79
+ warn "DEPRECATED: please use `enable_cable_ready_updates` instead. The `enable_updates` option will be removed from a future version of CableReady 5"
80
+ options.delete(:enable_updates)
81
+ else
82
+ options.delete(:enable_cable_ready_updates)
83
+ end
84
+
85
+ descendants = options.delete(:descendants)
86
+ debounce_time = options.delete(:debounce)
87
+
88
+ broadcast = option.present?
89
+ result = super
90
+ enrich_association_with_updates(name, option, descendants, debounce: debounce_time) if broadcast
91
+ result
92
+ end
93
+
94
+ def has_many_attached(name, **options)
95
+ raise("ActiveStorage must be enabled to use has_many_attached") unless defined?(ActiveStorage)
96
+
97
+ option = if options.has_key?(:enable_updates)
98
+ warn "DEPRECATED: please use `enable_cable_ready_updates` instead. The `enable_updates` option will be removed from a future version of CableReady 5"
99
+ options.delete(:enable_updates)
100
+ else
101
+ options.delete(:enable_cable_ready_updates)
102
+ end
103
+
104
+ debounce_time = options.delete(:debounce)
105
+
106
+ broadcast = option.present?
107
+ result = super
108
+ enrich_attachments_with_updates(name, option, debounce: debounce_time) if broadcast
109
+ result
110
+ end
111
+
112
+ def cable_ready_collections
113
+ @cable_ready_collections ||= CollectionsRegistry.new
114
+ end
115
+
116
+ def cable_ready_update_collection(resource, name, model, debounce: CableReady.config.updatable_debounce_time)
117
+ identifier = resource.to_global_id.to_s + ":" + name.to_s
118
+ changeset = model.respond_to?(:previous_changes) ? {changed: model.previous_changes.keys} : {}
119
+ options = changeset.merge({debounce: debounce})
120
+
121
+ broadcast_updates(identifier, options)
122
+ end
123
+
124
+ def enrich_association_with_updates(name, option, descendants = nil, debounce: CableReady.config.updatable_debounce_time)
125
+ reflection = reflect_on_association(name)
126
+
127
+ options = build_options(option)
128
+
129
+ [reflection.klass, *descendants&.map(&:to_s)&.map(&:constantize)].each do |klass|
130
+ klass.send(:include, CableReady::Updatable) unless klass.respond_to?(:cable_ready_collections)
131
+ klass.cable_ready_collections.register(Collection.new(
132
+ klass: self,
133
+ name: name,
134
+ options: options,
135
+ reflection: reflection,
136
+ debounce_time: debounce
137
+ ))
138
+ end
139
+ end
140
+
141
+ def enrich_attachments_with_updates(name, option, debounce: CableReady.config.updatable_debounce_time)
142
+ options = build_options(option)
143
+
144
+ ActiveStorage::Attachment.send(:include, CableReady::Updatable) unless ActiveStorage::Attachment.respond_to?(:cable_ready_collections)
145
+
146
+ ActiveStorage::Attachment.cable_ready_collections.register(Collection.new(
147
+ klass: self,
148
+ foreign_key: "record_id",
149
+ name: name,
150
+ inverse_association: "record",
151
+ through_association: nil,
152
+ options: options,
153
+ debounce_time: debounce
154
+ ))
155
+ end
156
+
157
+ def build_options(option)
158
+ options = {
159
+ on: [:create, :update, :destroy],
160
+ if: ->(resource) { true }
161
+ }
162
+
163
+ case option
164
+ when TrueClass
165
+ # proceed!
166
+ when FalseClass
167
+ options[:on] = []
168
+ when Array
169
+ options[:on] = option
170
+ when Symbol
171
+ options[:on] = [option]
172
+ when Hash
173
+ option[:on] = Array(option[:on]) if option[:on]
174
+ options = options.merge!(option)
175
+ when Proc
176
+ options[:if] = option
177
+ else
178
+ raise ArgumentError, "Invalid enable_cable_ready_updates option #{option}"
179
+ end
180
+
181
+ options
182
+ end
183
+
184
+ def broadcast_updates(model_class, options)
185
+ return if skip_updates_classes.any? { |klass| klass >= self }
186
+ raise("ActionCable must be enabled to use Updatable") unless defined?(ActionCable)
187
+
188
+ debounce_time = options.delete(:debounce)
189
+ debounce_time ||= CableReady.config.updatable_debounce_time
190
+
191
+ if debounce_time.to_f > 0
192
+ key = compound([model_class, *options])
193
+ old_wait_until = CableReady::Updatable.debounce_adapter[key]
194
+ now = Time.now.to_f
195
+
196
+ if old_wait_until.nil? || old_wait_until < now
197
+ new_wait_until = now + debounce_time.to_f
198
+ CableReady::Updatable.debounce_adapter[key] = new_wait_until
199
+ ActionCable.server.broadcast(model_class, options)
200
+ end
201
+ else
202
+ ActionCable.server.broadcast(model_class, options)
203
+ end
204
+ end
205
+
206
+ def skip_updates_classes
207
+ Thread.current[:skip_updates_classes] ||= []
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module ExtendHasMany
6
+ extend ::ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ def has_many(*args, &block)
10
+ options = args.extract_options!
11
+ options[:extend] = Array(options[:extend]).push(ClassMethods)
12
+ super(*args, **options, &block)
13
+ end
14
+ end
15
+ end
data/bin/standardize CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  bundle exec magic_frozen_string_literal
4
4
  bundle exec standardrb --fix
5
- yarn run prettier-standard ./javascript/*.js
5
+ yarn format
data/cable_ready.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require File.expand_path("../lib/cable_ready/version", __FILE__)
3
+ require File.expand_path("lib/cable_ready/version", __dir__)
4
4
 
5
5
  Gem::Specification.new do |gem|
6
6
  gem.name = "cable_ready"
@@ -8,19 +8,33 @@ Gem::Specification.new do |gem|
8
8
  gem.version = CableReady::VERSION
9
9
  gem.authors = ["Nathan Hopkins"]
10
10
  gem.email = ["natehop@gmail.com"]
11
- gem.homepage = "https://github.com/hopsoft/cable_ready"
11
+ gem.homepage = "https://github.com/stimulusreflex/cable_ready"
12
12
  gem.summary = "Out-of-Band Server Triggered DOM Operations"
13
13
 
14
- gem.files = Dir["lib/**/*.rb", "app/assets/javascripts/cable_ready.js", "bin/*", "[A-Z]*"]
15
- gem.test_files = Dir["test/**/*.rb"]
14
+ gem.files = Dir[
15
+ "lib/**/*.{rb,rake,tt}",
16
+ "app/**/*.rb",
17
+ "app/assets/javascripts/*",
18
+ "bin/*",
19
+ "[A-Z]*"
20
+ ]
16
21
 
17
- gem.add_dependency "rails", ">= 5.2"
22
+ gem.required_ruby_version = ">= 2.7.0"
23
+
24
+ rails_version = ">= 5.2"
25
+
26
+ gem.add_dependency "actionpack", rails_version
27
+ gem.add_dependency "actionview", rails_version
28
+ gem.add_dependency "activesupport", rails_version
29
+ gem.add_dependency "railties", rails_version
18
30
  gem.add_dependency "thread-local", ">= 1.1.0"
19
31
 
20
- gem.add_development_dependency "github_changelog_generator"
21
32
  gem.add_development_dependency "magic_frozen_string_literal"
33
+ gem.add_development_dependency "mocha"
22
34
  gem.add_development_dependency "pry"
23
35
  gem.add_development_dependency "pry-nav"
24
36
  gem.add_development_dependency "rake"
37
+ gem.add_development_dependency "sqlite3"
38
+ gem.add_development_dependency "standard", "1.19.1"
25
39
  gem.add_development_dependency "standardrb"
26
40
  end
@@ -1,17 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "channels"
3
+ require "active_support/concern"
4
4
 
5
5
  module CableReady
6
6
  module Broadcaster
7
+ include Identifiable
7
8
  extend ::ActiveSupport::Concern
8
9
 
9
10
  def cable_ready
10
11
  CableReady::Channels.instance
11
12
  end
12
13
 
13
- def dom_id(record, prefix = nil)
14
- "##{ActionView::RecordIdentifier.dom_id(record, prefix)}"
14
+ def cable_car
15
+ CableReady::CableCar.instance
15
16
  end
16
17
  end
17
18
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thread/local"
4
+
5
+ module CableReady
6
+ class CableCar < OperationBuilder
7
+ extend Thread::Local
8
+
9
+ def initialize
10
+ super "CableCar"
11
+ end
12
+
13
+ def dispatch(clear: true)
14
+ payload = operations_payload
15
+ reset! if clear
16
+ payload
17
+ end
18
+ end
19
+ end
@@ -1,47 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CableReady
4
- class Channel
5
- attr_reader :identifier, :enqueued_operations
6
-
7
- def initialize(identifier)
8
- @identifier = identifier
9
- reset
10
- CableReady.config.operation_names.each { |name| add_operation_method name }
11
-
12
- config_observer = self
13
- CableReady.config.add_observer config_observer, :add_operation_method
14
- ObjectSpace.define_finalizer self, -> { CableReady.config.delete_observer config_observer }
15
- end
4
+ class Channel < OperationBuilder
5
+ attr_reader :identifier
16
6
 
17
7
  def broadcast(clear: true)
18
- ActionCable.server.broadcast identifier, {"cableReady" => true, "operations" => broadcastable_operations}
19
- reset if clear
8
+ raise("Action Cable must be enabled to use broadcast") unless defined?(ActionCable)
9
+ clients_received = ActionCable.server.broadcast identifier, {
10
+ "cableReady" => true,
11
+ "operations" => operations_payload,
12
+ "version" => CableReady::VERSION
13
+ }
14
+ reset! if clear
15
+ clients_received
20
16
  end
21
17
 
22
18
  def broadcast_to(model, clear: true)
23
- identifier.broadcast_to model, {"cableReady" => true, "operations" => broadcastable_operations}
24
- reset if clear
25
- end
26
-
27
- def add_operation_method(name)
28
- return if respond_to?(name)
29
- singleton_class.public_send :define_method, name, ->(options = {}) {
30
- enqueued_operations[name.to_s] << options.stringify_keys
31
- self # supports operation chaining
19
+ raise("Action Cable must be enabled to use broadcast_to") unless defined?(ActionCable)
20
+ clients_received = identifier.broadcast_to model, {
21
+ "cableReady" => true,
22
+ "operations" => operations_payload,
23
+ "version" => CableReady::VERSION
32
24
  }
25
+ reset! if clear
26
+ clients_received
33
27
  end
34
28
 
35
- private
36
-
37
- def reset
38
- @enqueued_operations = Hash.new { |hash, key| hash[key] = [] }
29
+ def broadcast_later(clear: true, queue: nil)
30
+ raise("Action Cable must be enabled to use broadcast_later") unless defined?(ActionCable)
31
+ CableReady::BroadcastJob
32
+ .set(queue: queue ? queue.to_sym : CableReady.config.broadcast_job_queue)
33
+ .perform_later(identifier: identifier, operations: operations_payload)
34
+ reset! if clear
39
35
  end
40
36
 
41
- def broadcastable_operations
42
- enqueued_operations
43
- .select { |_, list| list.present? }
44
- .deep_transform_keys! { |key| key.to_s.camelize(:lower) }
37
+ def broadcast_later_to(model, clear: true, queue: nil)
38
+ raise("Action Cable must be enabled to use broadcast_later_to") unless defined?(ActionCable)
39
+ CableReady::BroadcastJob
40
+ .set(queue: queue ? queue.to_sym : CableReady.config.broadcast_job_queue)
41
+ .perform_later(identifier: identifier.name, operations: operations_payload, model: model)
42
+ reset! if clear
45
43
  end
46
44
  end
47
45
  end
@@ -1,22 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "thread/local"
4
- require_relative "channel"
5
4
 
6
5
  module CableReady
7
6
  # This class is a thread local singleton: CableReady::Channels.instance
8
7
  # SEE: https://github.com/socketry/thread-local/tree/master/guides/getting-started
9
8
  class Channels
9
+ include Compoundable
10
10
  extend Thread::Local
11
11
 
12
- attr_accessor :operations
13
-
14
12
  def initialize
15
13
  @channels = {}
16
- @operations = {}
17
14
  end
18
15
 
19
- def [](identifier)
16
+ def [](*keys)
17
+ keys.select!(&:itself)
18
+ identifier = (keys.many? || (keys.one? && keys.first.respond_to?(:to_global_id))) ? compound(keys) : keys.pop
20
19
  @channels[identifier] ||= CableReady::Channel.new(identifier)
21
20
  end
22
21
 
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CableReady
4
+ module Compoundable
5
+ def compound(keys)
6
+ keys.map { |key|
7
+ key.respond_to?(:to_global_id) ? key.to_global_id.to_s : key.to_s
8
+ }.join(":")
9
+ end
10
+ end
11
+ end