cable_ready 4.4.6 → 5.0.0.pre2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|