cable_ready 4.5.0 → 5.0.0.pre0
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 +194 -161
- data/Gemfile.lock +122 -85
- data/LATEST +1 -0
- data/README.md +9 -9
- data/Rakefile +8 -2
- data/bin/standardize +1 -1
- data/cable_ready.gemspec +2 -1
- data/lib/cable_ready.rb +37 -5
- data/lib/cable_ready/broadcaster.rb +3 -4
- data/lib/cable_ready/cable_car.rb +17 -0
- data/lib/cable_ready/channel.rb +12 -32
- data/lib/cable_ready/channels.rb +4 -7
- data/lib/cable_ready/compoundable.rb +11 -0
- data/lib/cable_ready/config.rb +15 -5
- 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/package.json +8 -5
- data/tags +21 -3
- 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
- data/yarn.lock +134 -124
- metadata +44 -7
@@ -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,47 +1,27 @@
|
|
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
|
-
ActionCable.server.broadcast identifier, {"cableReady" => true, "operations" =>
|
19
|
-
reset if clear
|
8
|
+
ActionCable.server.broadcast identifier, {"cableReady" => true, "operations" => operations_payload}
|
9
|
+
reset! if clear
|
20
10
|
end
|
21
11
|
|
22
12
|
def broadcast_to(model, clear: true)
|
23
|
-
identifier.broadcast_to model, {"cableReady" => true, "operations" =>
|
24
|
-
reset if clear
|
13
|
+
identifier.broadcast_to model, {"cableReady" => true, "operations" => operations_payload}
|
14
|
+
reset! if clear
|
25
15
|
end
|
26
16
|
|
27
|
-
def
|
28
|
-
|
29
|
-
|
30
|
-
enqueued_operations[name.to_s] << options.stringify_keys
|
31
|
-
self # supports operation chaining
|
32
|
-
}
|
33
|
-
end
|
34
|
-
|
35
|
-
private
|
36
|
-
|
37
|
-
def reset
|
38
|
-
@enqueued_operations = Hash.new { |hash, key| hash[key] = [] }
|
17
|
+
def broadcast_later(clear: true)
|
18
|
+
CableReadyBroadcastJob.perform_later(identifier: identifier, operations: operations_payload)
|
19
|
+
reset! if clear
|
39
20
|
end
|
40
21
|
|
41
|
-
def
|
42
|
-
|
43
|
-
|
44
|
-
.deep_transform_keys! { |key| key.to_s.camelize(:lower) }
|
22
|
+
def broadcast_later_to(model, clear: true)
|
23
|
+
CableReadyBroadcastJob.perform_later(identifier: identifier.name, operations: operations_payload, model: model)
|
24
|
+
reset! if clear
|
45
25
|
end
|
46
26
|
end
|
47
27
|
end
|
data/lib/cable_ready/channels.rb
CHANGED
@@ -1,22 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "thread/local"
|
4
|
-
require_relative "channel"
|
5
|
-
|
6
3
|
module CableReady
|
7
4
|
# This class is a thread local singleton: CableReady::Channels.instance
|
8
5
|
# SEE: https://github.com/socketry/thread-local/tree/master/guides/getting-started
|
9
6
|
class Channels
|
7
|
+
include Compoundable
|
10
8
|
extend Thread::Local
|
11
9
|
|
12
|
-
attr_accessor :operations
|
13
|
-
|
14
10
|
def initialize
|
15
11
|
@channels = {}
|
16
|
-
@operations = {}
|
17
12
|
end
|
18
13
|
|
19
|
-
def [](
|
14
|
+
def [](*keys)
|
15
|
+
keys.select!(&:itself)
|
16
|
+
identifier = keys.many? || (keys.one? && keys.first.is_a?(ActiveRecord::Base)) ? compound(keys) : keys.pop
|
20
17
|
@channels[identifier] ||= CableReady::Channel.new(identifier)
|
21
18
|
end
|
22
19
|
|
data/lib/cable_ready/config.rb
CHANGED
@@ -1,9 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "monitor"
|
4
|
-
require "observer"
|
5
|
-
require "singleton"
|
6
|
-
|
7
3
|
module CableReady
|
8
4
|
# This class is a process level singleton shared by all threads: CableReady::Config.instance
|
9
5
|
class Config
|
@@ -11,9 +7,22 @@ module CableReady
|
|
11
7
|
include Observable
|
12
8
|
include Singleton
|
13
9
|
|
10
|
+
attr_accessor :on_failed_sanity_checks, :on_new_version_available
|
11
|
+
attr_writer :verifier_key
|
12
|
+
|
14
13
|
def initialize
|
15
14
|
super
|
16
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")
|
17
26
|
end
|
18
27
|
|
19
28
|
def operation_names
|
@@ -33,6 +42,7 @@ module CableReady
|
|
33
42
|
append
|
34
43
|
clear_storage
|
35
44
|
console_log
|
45
|
+
console_table
|
36
46
|
dispatch_event
|
37
47
|
go
|
38
48
|
graft
|
@@ -42,7 +52,6 @@ module CableReady
|
|
42
52
|
morph
|
43
53
|
notification
|
44
54
|
outer_html
|
45
|
-
play_sound
|
46
55
|
prepend
|
47
56
|
push_state
|
48
57
|
remove
|
@@ -56,6 +65,7 @@ module CableReady
|
|
56
65
|
set_cookie
|
57
66
|
set_dataset_property
|
58
67
|
set_focus
|
68
|
+
set_meta
|
59
69
|
set_property
|
60
70
|
set_storage_item
|
61
71
|
set_style
|
@@ -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
|
@@ -0,0 +1,151 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CableReady::SanityChecker
|
4
|
+
LATEST_VERSION_FORMAT = /^(\d+\.\d+\.\d+)$/
|
5
|
+
NODE_VERSION_FORMAT = /(\d+\.\d+\.\d+.*):/
|
6
|
+
JSON_VERSION_FORMAT = /(\d+\.\d+\.\d+.*)"/
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def check!
|
10
|
+
return if CableReady.config.on_failed_sanity_checks == :ignore
|
11
|
+
return if called_by_generate_config?
|
12
|
+
|
13
|
+
instance = new
|
14
|
+
instance.check_javascript_package_version
|
15
|
+
instance.check_new_version_available
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def called_by_generate_config?
|
21
|
+
ARGV.include? "cable_ready:initializer"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def check_javascript_package_version
|
26
|
+
if javascript_package_version.nil?
|
27
|
+
warn_and_exit <<~WARN
|
28
|
+
Can't locate the cable_ready npm package.
|
29
|
+
Either add it to your package.json as a dependency or use "yarn link cable_ready" if you are doing development.
|
30
|
+
WARN
|
31
|
+
end
|
32
|
+
|
33
|
+
unless javascript_version_matches?
|
34
|
+
warn_and_exit <<~WARN
|
35
|
+
The cable_ready npm package version (#{javascript_package_version}) does not match the Rubygem version (#{gem_version}).
|
36
|
+
To update the cable_ready npm package:
|
37
|
+
yarn upgrade cable_ready@#{gem_version}
|
38
|
+
WARN
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def check_new_version_available
|
43
|
+
return unless Rails.env.development?
|
44
|
+
return if CableReady.config.on_new_version_available == :ignore
|
45
|
+
return unless using_stable_release
|
46
|
+
begin
|
47
|
+
latest_version = URI.open("https://raw.githubusercontent.com/stimulusreflex/cable_ready/master/LATEST", open_timeout: 1, read_timeout: 1).read.strip
|
48
|
+
if latest_version != CableReady::VERSION
|
49
|
+
puts <<~WARN
|
50
|
+
|
51
|
+
There is a new version of CableReady available!
|
52
|
+
Current: #{CableReady::VERSION} Latest: #{latest_version}
|
53
|
+
|
54
|
+
If you upgrade, it is very important that you update BOTH Gemfile and package.json
|
55
|
+
Then, run `bundle install && yarn install` to update to #{latest_version}.
|
56
|
+
|
57
|
+
WARN
|
58
|
+
exit if CableReady.config.on_new_version_available == :exit
|
59
|
+
end
|
60
|
+
rescue
|
61
|
+
puts "CableReady #{CableReady::VERSION} update check skipped: connection timeout"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def javascript_version_matches?
|
68
|
+
javascript_package_version == gem_version
|
69
|
+
end
|
70
|
+
|
71
|
+
def using_stable_release
|
72
|
+
stable = CableReady::VERSION.match?(LATEST_VERSION_FORMAT)
|
73
|
+
puts "CableReady #{CableReady::VERSION} update check skipped: pre-release build" unless stable
|
74
|
+
stable
|
75
|
+
end
|
76
|
+
|
77
|
+
def gem_version
|
78
|
+
@_gem_version ||= CableReady::VERSION.gsub(".pre", "-pre")
|
79
|
+
end
|
80
|
+
|
81
|
+
def javascript_package_version
|
82
|
+
@_js_version ||= find_javascript_package_version
|
83
|
+
end
|
84
|
+
|
85
|
+
def find_javascript_package_version
|
86
|
+
if (match = search_file(package_json_path, regex: /version/))
|
87
|
+
match[JSON_VERSION_FORMAT, 1]
|
88
|
+
elsif (match = search_file(yarn_lock_path, regex: /^cable_ready/))
|
89
|
+
match[NODE_VERSION_FORMAT, 1]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def search_file(path, regex:)
|
94
|
+
return unless File.exist?(path)
|
95
|
+
File.foreach(path).grep(regex).first
|
96
|
+
end
|
97
|
+
|
98
|
+
def package_json_path
|
99
|
+
Rails.root.join("node_modules", "cable_ready", "package.json")
|
100
|
+
end
|
101
|
+
|
102
|
+
def yarn_lock_path
|
103
|
+
Rails.root.join("yarn.lock")
|
104
|
+
end
|
105
|
+
|
106
|
+
def initializer_path
|
107
|
+
@_initializer_path ||= Rails.root.join("config", "initializers", "cable_ready.rb")
|
108
|
+
end
|
109
|
+
|
110
|
+
def warn_and_exit(text)
|
111
|
+
puts "WARNING:"
|
112
|
+
puts text
|
113
|
+
exit_with_info if CableReady.config.on_failed_sanity_checks == :exit
|
114
|
+
end
|
115
|
+
|
116
|
+
def exit_with_info
|
117
|
+
puts
|
118
|
+
|
119
|
+
if File.exist?(initializer_path)
|
120
|
+
puts <<~INFO
|
121
|
+
If you know what you are doing and you want to start the application anyway,
|
122
|
+
you can add the following directive to the CableReady initializer,
|
123
|
+
which is located at #{initializer_path}
|
124
|
+
|
125
|
+
CableReady.configure do |config|
|
126
|
+
config.on_failed_sanity_checks = :warn
|
127
|
+
end
|
128
|
+
|
129
|
+
INFO
|
130
|
+
else
|
131
|
+
puts <<~INFO
|
132
|
+
If you know what you are doing and you want to start the application anyway,
|
133
|
+
you can create a CableReady initializer with the command:
|
134
|
+
|
135
|
+
bundle exec rails generate cable_ready:config
|
136
|
+
|
137
|
+
Then open your initializer at
|
138
|
+
|
139
|
+
#{initializer_path}
|
140
|
+
|
141
|
+
and then add the following directive:
|
142
|
+
|
143
|
+
CableReady.configure do |config|
|
144
|
+
config.on_failed_sanity_checks = :warn
|
145
|
+
end
|
146
|
+
|
147
|
+
INFO
|
148
|
+
end
|
149
|
+
exit false unless Rails.env.test?
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CableReady
|
4
|
+
module StreamIdentifier
|
5
|
+
def verified_stream_identifier(signed_stream_identifier)
|
6
|
+
CableReady.signed_stream_verifier.verified signed_stream_identifier
|
7
|
+
end
|
8
|
+
|
9
|
+
def signed_stream_identifier(compoundable)
|
10
|
+
CableReady.signed_stream_verifier.generate compoundable
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|