cable_ready 4.4.6 → 5.0.0.pre2
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 +228 -161
- data/Gemfile.lock +146 -99
- 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/cable_ready.gemspec +4 -2
- data/lib/cable_ready.rb +44 -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 -68
- data/lib/cable_ready/compoundable.rb +11 -0
- data/lib/cable_ready/config.rb +78 -0
- data/lib/cable_ready/identifiable.rb +30 -0
- data/lib/cable_ready/operation_builder.rb +83 -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/package.json +10 -5
- data/tags +58 -35
- data/test/lib/cable_ready/cable_car_test.rb +50 -0
- data/test/lib/cable_ready/identifiable_test.rb +75 -0
- data/test/lib/cable_ready/operation_builder_test.rb +211 -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
- data/yarn.lock +134 -124
- metadata +66 -11
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CableReadyHelper
|
4
|
+
include CableReady::Compoundable
|
5
|
+
include CableReady::StreamIdentifier
|
6
|
+
|
7
|
+
def stream_from(*keys)
|
8
|
+
keys.select!(&:itself)
|
9
|
+
tag.stream_from(identifier: signed_stream_identifier(compound(keys)))
|
10
|
+
end
|
11
|
+
end
|
@@ -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/cable_ready.gemspec
CHANGED
@@ -8,13 +8,14 @@ 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["lib/**/*.rb", "app
|
14
|
+
gem.files = Dir["lib/**/*.rb", "app/**/*.rb", "bin/*", "[A-Z]*"]
|
15
15
|
gem.test_files = Dir["test/**/*.rb"]
|
16
16
|
|
17
17
|
gem.add_dependency "rails", ">= 5.2"
|
18
|
+
gem.add_dependency "thread-local", ">= 1.1.0"
|
18
19
|
|
19
20
|
gem.add_development_dependency "github_changelog_generator"
|
20
21
|
gem.add_development_dependency "magic_frozen_string_literal"
|
@@ -22,4 +23,5 @@ Gem::Specification.new do |gem|
|
|
22
23
|
gem.add_development_dependency "pry-nav"
|
23
24
|
gem.add_development_dependency "rake"
|
24
25
|
gem.add_development_dependency "standardrb"
|
26
|
+
gem.add_development_dependency "mocha"
|
25
27
|
end
|
data/lib/cable_ready.rb
CHANGED
@@ -1,11 +1,55 @@
|
|
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
|
+
response.content_type ||= Mime[:cable_ready]
|
34
|
+
render json: operations.dispatch
|
35
|
+
end
|
36
|
+
|
37
|
+
Mime::Type.register "application/vnd.cable-ready.json", :cable_ready
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class << self
|
43
|
+
def config
|
44
|
+
CableReady::Config.instance
|
45
|
+
end
|
46
|
+
|
47
|
+
def configure
|
48
|
+
yield config
|
49
|
+
end
|
50
|
+
|
51
|
+
def signed_stream_verifier
|
52
|
+
@signed_stream_verifier ||= ActiveSupport::MessageVerifier.new(config.verifier_key, digest: "SHA256", serializer: JSON)
|
53
|
+
end
|
10
54
|
end
|
11
55
|
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,86 +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
|
-
clear_storage
|
20
|
-
console_log
|
21
|
-
dispatch_event
|
22
|
-
inner_html
|
23
|
-
insert_adjacent_html
|
24
|
-
insert_adjacent_text
|
25
|
-
morph
|
26
|
-
notification
|
27
|
-
outer_html
|
28
|
-
push_state
|
29
|
-
remove
|
30
|
-
remove_attribute
|
31
|
-
remove_css_class
|
32
|
-
remove_storage_item
|
33
|
-
set_attribute
|
34
|
-
set_cookie
|
35
|
-
set_dataset_property
|
36
|
-
set_focus
|
37
|
-
set_property
|
38
|
-
set_storage_item
|
39
|
-
set_style
|
40
|
-
set_styles
|
41
|
-
set_value
|
42
|
-
text_content
|
43
|
-
].each do |operation|
|
44
|
-
add_operation operation
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
def add_operation(operation, &implementation)
|
49
|
-
@operations[operation] = implementation || ->(options = {}) { enqueue_operation(operation, options) }
|
50
12
|
end
|
51
13
|
|
52
|
-
def [](
|
53
|
-
|
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)
|
54
18
|
end
|
55
19
|
|
56
20
|
def broadcast(*identifiers, clear: true)
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
.
|
62
|
-
|
63
|
-
|
64
|
-
end
|
65
|
-
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
|
66
28
|
end
|
67
29
|
|
68
30
|
def broadcast_to(model, *identifiers, clear: true)
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
.
|
74
|
-
|
75
|
-
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
private
|
81
|
-
|
82
|
-
def mutex
|
83
|
-
@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
|
84
38
|
end
|
85
39
|
end
|
86
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,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CableReady
|
4
|
+
module Identifiable
|
5
|
+
def dom_id(record, prefix = nil)
|
6
|
+
return record.to_dom_selector if record.respond_to?(:to_dom_selector)
|
7
|
+
|
8
|
+
prefix = prefix.to_s.strip if prefix
|
9
|
+
|
10
|
+
id = if record.respond_to?(:to_dom_id)
|
11
|
+
record.to_dom_id
|
12
|
+
elsif record.is_a?(ActiveRecord::Relation)
|
13
|
+
[prefix, record.model_name.plural].compact.join("_")
|
14
|
+
elsif record.is_a?(ActiveRecord::Base)
|
15
|
+
ActionView::RecordIdentifier.dom_id(record, prefix)
|
16
|
+
else
|
17
|
+
[prefix, record.to_s.strip].compact.join("_")
|
18
|
+
end
|
19
|
+
|
20
|
+
"##{id}".squeeze("#").strip.downcase
|
21
|
+
end
|
22
|
+
|
23
|
+
def identifiable?(obj)
|
24
|
+
obj.respond_to?(:to_dom_selector) ||
|
25
|
+
obj.respond_to?(:to_dom_id) ||
|
26
|
+
obj.is_a?(ActiveRecord::Relation) ||
|
27
|
+
obj.is_a?(ActiveRecord::Base)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|