cable_ready 4.4.3 → 5.0.0.pre1
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 +238 -155
- data/Gemfile.lock +144 -100
- data/LATEST +1 -0
- data/README.md +13 -13
- data/Rakefile +8 -2
- data/app/channels/cable_ready/stream.rb +12 -0
- data/app/helpers/cable_ready_helper.rb +11 -0
- data/app/jobs/cable_ready_broadcast_job.rb +14 -0
- data/bin/standardize +1 -1
- data/lib/cable_ready.rb +41 -0
- data/lib/cable_ready/broadcaster.rb +3 -4
- data/lib/cable_ready/cable_car.rb +17 -0
- data/lib/cable_ready/channel.rb +14 -36
- data/lib/cable_ready/channels.rb +22 -65
- data/lib/cable_ready/compoundable.rb +11 -0
- data/lib/cable_ready/config.rb +78 -0
- data/lib/cable_ready/identifiable.rb +19 -0
- data/lib/cable_ready/operation_builder.rb +69 -0
- data/lib/cable_ready/sanity_checker.rb +151 -0
- data/lib/cable_ready/stream_identifier.rb +13 -0
- data/lib/cable_ready/version.rb +1 -1
- data/lib/generators/cable_ready/channel_generator.rb +71 -0
- data/lib/generators/cable_ready/initializer_generator.rb +14 -0
- data/lib/generators/cable_ready/stream_from_generator.rb +43 -0
- data/lib/generators/cable_ready/templates/config/initializers/cable_ready.rb +18 -0
- data/test/lib/cable_ready/cable_car_test.rb +28 -0
- data/test/lib/cable_ready/identifiable_test.rb +75 -0
- data/test/lib/cable_ready/operation_builder_test.rb +128 -0
- data/test/lib/generators/cable_ready/channel_generator_test.rb +157 -0
- data/test/support/generator_test_helpers.rb +28 -0
- data/test/test_helper.rb +15 -0
- metadata +66 -15
- data/cable_ready.gemspec +0 -25
- data/package.json +0 -36
- data/tags +0 -57
- data/yarn.lock +0 -2552
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CableReadyBroadcastJob < (defined?(ActiveJob::Base) ? ActiveJob::Base : Object)
|
|
4
|
+
include CableReady::Broadcaster
|
|
5
|
+
queue_as :default if defined?(ActiveJob::Base)
|
|
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
|
data/bin/standardize
CHANGED
data/lib/cable_ready.rb
CHANGED
|
@@ -1,11 +1,52 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "rails/engine"
|
|
4
|
+
require "open-uri"
|
|
5
|
+
require "active_record"
|
|
6
|
+
require "action_view"
|
|
4
7
|
require "active_support/all"
|
|
8
|
+
require "thread/local"
|
|
9
|
+
require "monitor"
|
|
10
|
+
require "observer"
|
|
11
|
+
require "singleton"
|
|
5
12
|
require "cable_ready/version"
|
|
13
|
+
require "cable_ready/identifiable"
|
|
14
|
+
require "cable_ready/operation_builder"
|
|
15
|
+
require "cable_ready/config"
|
|
6
16
|
require "cable_ready/broadcaster"
|
|
17
|
+
require "cable_ready/sanity_checker"
|
|
18
|
+
require "cable_ready/compoundable"
|
|
19
|
+
require "cable_ready/channel"
|
|
20
|
+
require "cable_ready/channels"
|
|
21
|
+
require "cable_ready/cable_car"
|
|
22
|
+
require "cable_ready/stream_identifier"
|
|
7
23
|
|
|
8
24
|
module CableReady
|
|
9
25
|
class Engine < Rails::Engine
|
|
26
|
+
initializer "cable_ready.sanity_check" do
|
|
27
|
+
SanityChecker.check! unless Rails.env.production?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
initializer "renderer" do
|
|
31
|
+
ActiveSupport.on_load(:action_controller) do
|
|
32
|
+
ActionController::Renderers.add :operations do |operations, options|
|
|
33
|
+
render json: operations.dispatch
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class << self
|
|
40
|
+
def config
|
|
41
|
+
CableReady::Config.instance
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def configure
|
|
45
|
+
yield config
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def signed_stream_verifier
|
|
49
|
+
@signed_stream_verifier ||= ActiveSupport::MessageVerifier.new(config.verifier_key, digest: "SHA256", serializer: JSON)
|
|
50
|
+
end
|
|
10
51
|
end
|
|
11
52
|
end
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "channels"
|
|
4
|
-
|
|
5
3
|
module CableReady
|
|
6
4
|
module Broadcaster
|
|
5
|
+
include Identifiable
|
|
7
6
|
extend ::ActiveSupport::Concern
|
|
8
7
|
|
|
9
8
|
def cable_ready
|
|
10
9
|
CableReady::Channels.instance
|
|
11
10
|
end
|
|
12
11
|
|
|
13
|
-
def
|
|
14
|
-
|
|
12
|
+
def cable_car
|
|
13
|
+
CableReady::CableCar.instance
|
|
15
14
|
end
|
|
16
15
|
end
|
|
17
16
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CableReady
|
|
4
|
+
class CableCar < OperationBuilder
|
|
5
|
+
extend Thread::Local
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
super "CableCar"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def dispatch(clear: true)
|
|
12
|
+
payload = operations_payload
|
|
13
|
+
reset! if clear
|
|
14
|
+
payload
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/cable_ready/channel.rb
CHANGED
|
@@ -1,49 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CableReady
|
|
4
|
-
class Channel
|
|
5
|
-
attr_reader :identifier
|
|
4
|
+
class Channel < OperationBuilder
|
|
5
|
+
attr_reader :identifier
|
|
6
6
|
|
|
7
|
-
def
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
reset
|
|
11
|
-
available_operations.each do |available_operation, implementation|
|
|
12
|
-
define_singleton_method available_operation, &implementation
|
|
13
|
-
end
|
|
7
|
+
def broadcast(clear: true)
|
|
8
|
+
ActionCable.server.broadcast identifier, {"cableReady" => true, "operations" => operations_payload}
|
|
9
|
+
reset! if clear
|
|
14
10
|
end
|
|
15
11
|
|
|
16
|
-
def
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
ActionCable.server.broadcast identifier, {"cableReady" => true, "operations" => operations}
|
|
20
|
-
reset if clear
|
|
12
|
+
def broadcast_to(model, clear: true)
|
|
13
|
+
identifier.broadcast_to model, {"cableReady" => true, "operations" => operations_payload}
|
|
14
|
+
reset! if clear
|
|
21
15
|
end
|
|
22
16
|
|
|
23
|
-
def
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
identifier.broadcast_to model, {"cableReady" => true, "operations" => operations}
|
|
27
|
-
reset if clear
|
|
17
|
+
def broadcast_later(clear: true)
|
|
18
|
+
CableReadyBroadcastJob.perform_later(identifier: identifier, operations: operations_payload)
|
|
19
|
+
reset! if clear
|
|
28
20
|
end
|
|
29
21
|
|
|
30
|
-
def
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def broadcast_to(model, clear = true)
|
|
35
|
-
CableReady::Channels.instance.broadcast_to(model, identifier, clear: clear)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
private
|
|
39
|
-
|
|
40
|
-
def enqueue_operation(key, options)
|
|
41
|
-
operations[key] << options
|
|
42
|
-
self
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def reset
|
|
46
|
-
@operations = Hash.new { |hash, operation| hash[operation] = [] }
|
|
22
|
+
def broadcast_later_to(model, clear: true)
|
|
23
|
+
CableReadyBroadcastJob.perform_later(identifier: identifier.name, operations: operations_payload, model: model)
|
|
24
|
+
reset! if clear
|
|
47
25
|
end
|
|
48
26
|
end
|
|
49
27
|
end
|
data/lib/cable_ready/channels.rb
CHANGED
|
@@ -1,83 +1,40 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "channel"
|
|
4
|
-
|
|
5
3
|
module CableReady
|
|
4
|
+
# This class is a thread local singleton: CableReady::Channels.instance
|
|
5
|
+
# SEE: https://github.com/socketry/thread-local/tree/master/guides/getting-started
|
|
6
6
|
class Channels
|
|
7
|
-
include
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def self.configure
|
|
11
|
-
yield CableReady::Channels.instance if block_given?
|
|
12
|
-
end
|
|
7
|
+
include Compoundable
|
|
8
|
+
extend Thread::Local
|
|
13
9
|
|
|
14
10
|
def initialize
|
|
15
11
|
@channels = {}
|
|
16
|
-
@operations = {}
|
|
17
|
-
%i[
|
|
18
|
-
add_css_class
|
|
19
|
-
console_log
|
|
20
|
-
dispatch_event
|
|
21
|
-
inner_html
|
|
22
|
-
insert_adjacent_html
|
|
23
|
-
insert_adjacent_text
|
|
24
|
-
morph
|
|
25
|
-
notification
|
|
26
|
-
outer_html
|
|
27
|
-
push_state
|
|
28
|
-
remove
|
|
29
|
-
remove_attribute
|
|
30
|
-
remove_css_class
|
|
31
|
-
set_attribute
|
|
32
|
-
set_cookie
|
|
33
|
-
set_dataset_property
|
|
34
|
-
set_focus
|
|
35
|
-
set_property
|
|
36
|
-
set_style
|
|
37
|
-
set_styles
|
|
38
|
-
set_value
|
|
39
|
-
text_content
|
|
40
|
-
].each do |operation|
|
|
41
|
-
add_operation operation
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def add_operation(operation, &implementation)
|
|
46
|
-
@operations[operation] = implementation || ->(options = {}) { enqueue_operation(operation, options) }
|
|
47
12
|
end
|
|
48
13
|
|
|
49
|
-
def [](
|
|
50
|
-
|
|
14
|
+
def [](*keys)
|
|
15
|
+
keys.select!(&:itself)
|
|
16
|
+
identifier = keys.many? || (keys.one? && keys.first.is_a?(ActiveRecord::Base)) ? compound(keys) : keys.pop
|
|
17
|
+
@channels[identifier] ||= CableReady::Channel.new(identifier)
|
|
51
18
|
end
|
|
52
19
|
|
|
53
20
|
def broadcast(*identifiers, clear: true)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
end
|
|
62
|
-
end
|
|
21
|
+
@channels.values
|
|
22
|
+
.reject { |channel| identifiers.any? && identifiers.exclude?(channel.identifier) }
|
|
23
|
+
.select { |channel| channel.identifier.is_a?(String) }
|
|
24
|
+
.tap do |channels|
|
|
25
|
+
channels.each { |channel| @channels[channel.identifier].broadcast(clear: clear) }
|
|
26
|
+
channels.each { |channel| @channels.except!(channel.identifier) if clear }
|
|
27
|
+
end
|
|
63
28
|
end
|
|
64
29
|
|
|
65
30
|
def broadcast_to(model, *identifiers, clear: true)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
private
|
|
78
|
-
|
|
79
|
-
def mutex
|
|
80
|
-
@mutex ||= Mutex.new
|
|
31
|
+
@channels.values
|
|
32
|
+
.reject { |channel| identifiers.any? && identifiers.exclude?(channel.identifier) }
|
|
33
|
+
.reject { |channel| channel.identifier.is_a?(String) }
|
|
34
|
+
.tap do |channels|
|
|
35
|
+
channels.each { |channel| @channels[channel.identifier].broadcast_to(model, clear: clear) }
|
|
36
|
+
channels.each { |channel| @channels.except!(channel.identifier) if clear }
|
|
37
|
+
end
|
|
81
38
|
end
|
|
82
39
|
end
|
|
83
40
|
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CableReady
|
|
4
|
+
# This class is a process level singleton shared by all threads: CableReady::Config.instance
|
|
5
|
+
class Config
|
|
6
|
+
include MonitorMixin
|
|
7
|
+
include Observable
|
|
8
|
+
include Singleton
|
|
9
|
+
|
|
10
|
+
attr_accessor :on_failed_sanity_checks, :on_new_version_available
|
|
11
|
+
attr_writer :verifier_key
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
super
|
|
15
|
+
@operation_names = Set.new(default_operation_names)
|
|
16
|
+
@on_failed_sanity_checks = :exit
|
|
17
|
+
@on_new_version_available = :ignore
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def observers
|
|
21
|
+
@observer_peers&.keys || []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def verifier_key
|
|
25
|
+
@verifier_key || Rails.application.key_generator.generate_key("cable_ready/verifier_key")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def operation_names
|
|
29
|
+
@operation_names.to_a
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def add_operation_name(name)
|
|
33
|
+
synchronize do
|
|
34
|
+
@operation_names << name.to_sym
|
|
35
|
+
notify_observers name.to_sym
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def default_operation_names
|
|
40
|
+
Set.new(%i[
|
|
41
|
+
add_css_class
|
|
42
|
+
append
|
|
43
|
+
clear_storage
|
|
44
|
+
console_log
|
|
45
|
+
console_table
|
|
46
|
+
dispatch_event
|
|
47
|
+
go
|
|
48
|
+
graft
|
|
49
|
+
inner_html
|
|
50
|
+
insert_adjacent_html
|
|
51
|
+
insert_adjacent_text
|
|
52
|
+
morph
|
|
53
|
+
notification
|
|
54
|
+
outer_html
|
|
55
|
+
prepend
|
|
56
|
+
push_state
|
|
57
|
+
remove
|
|
58
|
+
remove_attribute
|
|
59
|
+
remove_css_class
|
|
60
|
+
remove_storage_item
|
|
61
|
+
replace
|
|
62
|
+
replace_state
|
|
63
|
+
scroll_into_view
|
|
64
|
+
set_attribute
|
|
65
|
+
set_cookie
|
|
66
|
+
set_dataset_property
|
|
67
|
+
set_focus
|
|
68
|
+
set_meta
|
|
69
|
+
set_property
|
|
70
|
+
set_storage_item
|
|
71
|
+
set_style
|
|
72
|
+
set_styles
|
|
73
|
+
set_value
|
|
74
|
+
text_content
|
|
75
|
+
]).freeze
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CableReady
|
|
4
|
+
module Identifiable
|
|
5
|
+
def dom_id(record, prefix = nil)
|
|
6
|
+
prefix = prefix.to_s.strip if prefix
|
|
7
|
+
|
|
8
|
+
id = if record.is_a?(ActiveRecord::Relation)
|
|
9
|
+
[prefix, record.model_name.plural].compact.join("_")
|
|
10
|
+
elsif record.is_a?(ActiveRecord::Base)
|
|
11
|
+
ActionView::RecordIdentifier.dom_id(record, prefix)
|
|
12
|
+
else
|
|
13
|
+
[prefix, record.to_s.strip].compact.join("_")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
"##{id}".squeeze("#").strip
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CableReady
|
|
4
|
+
class OperationBuilder
|
|
5
|
+
include Identifiable
|
|
6
|
+
attr_reader :identifier, :previous_selector
|
|
7
|
+
|
|
8
|
+
def self.finalizer_for(identifier)
|
|
9
|
+
proc {
|
|
10
|
+
channel = CableReady.config.observers.find { |o| o.try(:identifier) == identifier }
|
|
11
|
+
CableReady.config.delete_observer channel if channel
|
|
12
|
+
}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(identifier)
|
|
16
|
+
@identifier = identifier
|
|
17
|
+
|
|
18
|
+
reset!
|
|
19
|
+
CableReady.config.operation_names.each { |name| add_operation_method name }
|
|
20
|
+
CableReady.config.add_observer self, :add_operation_method
|
|
21
|
+
ObjectSpace.define_finalizer self, self.class.finalizer_for(identifier)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def add_operation_method(name)
|
|
25
|
+
return if respond_to?(name)
|
|
26
|
+
singleton_class.public_send :define_method, name, ->(*args) {
|
|
27
|
+
selector, options = nil, args.first || {} # 1 or 0 params
|
|
28
|
+
selector, options = options, {} unless options.is_a?(Hash) # swap if only selector provided
|
|
29
|
+
selector, options = args[0, 2] if args.many? # 2 or more params
|
|
30
|
+
options.stringify_keys!
|
|
31
|
+
options["selector"] = selector if selector && options.exclude?("selector")
|
|
32
|
+
options["selector"] = previous_selector if previous_selector && options.exclude?("selector")
|
|
33
|
+
if options.include?("selector")
|
|
34
|
+
@previous_selector = options["selector"]
|
|
35
|
+
options["selector"] = previous_selector.is_a?(ActiveRecord::Base) || previous_selector.is_a?(ActiveRecord::Relation) ? dom_id(previous_selector) : previous_selector
|
|
36
|
+
end
|
|
37
|
+
@enqueued_operations[name.to_s] << options
|
|
38
|
+
self
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def to_json(*args)
|
|
43
|
+
@enqueued_operations.to_json(*args)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def apply!(operations = "{}")
|
|
47
|
+
operations = begin
|
|
48
|
+
JSON.parse(operations.is_a?(String) ? operations : operations.to_json)
|
|
49
|
+
rescue JSON::ParserError
|
|
50
|
+
{}
|
|
51
|
+
end
|
|
52
|
+
operations.each do |name, operation|
|
|
53
|
+
operation.each do |enqueued_operation|
|
|
54
|
+
@enqueued_operations[name.to_s] << enqueued_operation
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
self
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def operations_payload
|
|
61
|
+
@enqueued_operations.select { |_, list| list.present? }.deep_transform_keys { |key| key.to_s.camelize(:lower) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def reset!
|
|
65
|
+
@enqueued_operations = Hash.new { |hash, key| hash[key] = [] }
|
|
66
|
+
@previous_selector = nil
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|