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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +2 -376
- data/Gemfile +4 -1
- data/Gemfile.lock +146 -144
- data/README.md +54 -20
- data/Rakefile +8 -8
- data/app/assets/javascripts/cable_ready.js +1269 -0
- data/app/assets/javascripts/cable_ready.umd.js +1190 -0
- data/app/channels/cable_ready/stream.rb +14 -0
- data/app/helpers/cable_ready/view_helper.rb +58 -0
- data/app/jobs/cable_ready/broadcast_job.rb +15 -0
- data/app/models/concerns/cable_ready/updatable/collection_updatable_callbacks.rb +21 -0
- data/app/models/concerns/cable_ready/updatable/collections_registry.rb +59 -0
- data/app/models/concerns/cable_ready/updatable/memory_cache_debounce_adapter.rb +24 -0
- data/app/models/concerns/cable_ready/updatable/model_updatable_callbacks.rb +33 -0
- data/app/models/concerns/cable_ready/updatable.rb +211 -0
- data/app/models/concerns/extend_has_many.rb +15 -0
- data/bin/standardize +1 -1
- data/cable_ready.gemspec +20 -6
- data/lib/cable_ready/broadcaster.rb +4 -3
- data/lib/cable_ready/cable_car.rb +19 -0
- data/lib/cable_ready/channel.rb +29 -31
- data/lib/cable_ready/channels.rb +4 -5
- data/lib/cable_ready/compoundable.rb +11 -0
- data/lib/cable_ready/config.rb +28 -1
- data/lib/cable_ready/engine.rb +59 -0
- data/lib/cable_ready/identifiable.rb +48 -0
- data/lib/cable_ready/importmap.rb +4 -0
- data/lib/cable_ready/installer.rb +224 -0
- data/lib/cable_ready/operation_builder.rb +80 -0
- data/lib/cable_ready/sanity_checker.rb +63 -0
- data/lib/cable_ready/stream_identifier.rb +13 -0
- data/lib/cable_ready/version.rb +1 -1
- data/lib/cable_ready.rb +23 -10
- data/lib/cable_ready_helper.rb +13 -0
- data/lib/generators/cable_ready/channel_generator.rb +110 -0
- data/lib/generators/cable_ready/templates/app/javascript/channels/consumer.js.tt +6 -0
- data/lib/generators/cable_ready/templates/app/javascript/channels/index.js.esbuild.tt +4 -0
- data/lib/generators/cable_ready/templates/app/javascript/channels/index.js.importmap.tt +2 -0
- data/lib/generators/cable_ready/templates/app/javascript/channels/index.js.shakapacker.tt +5 -0
- data/lib/generators/cable_ready/templates/app/javascript/channels/index.js.vite.tt +1 -0
- data/lib/generators/cable_ready/templates/app/javascript/channels/index.js.webpacker.tt +5 -0
- data/lib/generators/cable_ready/templates/app/javascript/config/cable_ready.js.tt +4 -0
- data/lib/generators/cable_ready/templates/app/javascript/config/index.js.tt +1 -0
- data/lib/generators/cable_ready/templates/app/javascript/config/mrujs.js.tt +9 -0
- data/lib/generators/cable_ready/templates/app/javascript/controllers/%file_name%_controller.js.tt +38 -0
- data/lib/generators/cable_ready/templates/app/javascript/controllers/application.js.tt +11 -0
- data/lib/generators/cable_ready/templates/app/javascript/controllers/index.js.esbuild.tt +7 -0
- data/lib/generators/cable_ready/templates/app/javascript/controllers/index.js.importmap.tt +5 -0
- data/lib/generators/cable_ready/templates/app/javascript/controllers/index.js.shakapacker.tt +5 -0
- data/lib/generators/cable_ready/templates/app/javascript/controllers/index.js.vite.tt +5 -0
- data/lib/generators/cable_ready/templates/app/javascript/controllers/index.js.webpacker.tt +5 -0
- data/lib/generators/cable_ready/templates/config/initializers/cable_ready.rb +27 -0
- data/lib/generators/cable_ready/templates/esbuild.config.mjs.tt +94 -0
- data/lib/install/action_cable.rb +144 -0
- data/lib/install/broadcaster.rb +109 -0
- data/lib/install/bundle.rb +54 -0
- data/lib/install/compression.rb +51 -0
- data/lib/install/config.rb +39 -0
- data/lib/install/development.rb +34 -0
- data/lib/install/esbuild.rb +101 -0
- data/lib/install/importmap.rb +96 -0
- data/lib/install/initializers.rb +15 -0
- data/lib/install/mrujs.rb +121 -0
- data/lib/install/npm_packages.rb +13 -0
- data/lib/install/shakapacker.rb +65 -0
- data/lib/install/spring.rb +54 -0
- data/lib/install/updatable.rb +34 -0
- data/lib/install/vite.rb +66 -0
- data/lib/install/webpacker.rb +93 -0
- data/lib/install/yarn.rb +56 -0
- data/lib/tasks/cable_ready/cable_ready.rake +247 -0
- data/package.json +42 -13
- data/rollup.config.mjs +57 -0
- data/web-test-runner.config.mjs +12 -0
- data/yarn.lock +3252 -327
- metadata +138 -9
- 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
data/cable_ready.gemspec
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require File.expand_path("
|
|
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/
|
|
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[
|
|
15
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
14
|
-
|
|
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
|
data/lib/cable_ready/channel.rb
CHANGED
|
@@ -1,47 +1,45 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CableReady
|
|
4
|
-
class Channel
|
|
5
|
-
attr_reader :identifier
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.
|
|
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
|
data/lib/cable_ready/channels.rb
CHANGED
|
@@ -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 [](
|
|
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
|
|