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,83 @@
|
|
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
|
+
if args.one? && args.first.respond_to?(:to_operation_options) && [Array, Hash].include?(args.first.to_operation_options.class)
|
28
|
+
case args.first.to_operation_options
|
29
|
+
when Array
|
30
|
+
selector, options = nil, args.first.to_operation_options
|
31
|
+
.select { |e| e.is_a?(Symbol) && args.first.respond_to?("to_#{e}".to_sym) }
|
32
|
+
.each_with_object({}) { |option, memo| memo[option.to_s] = args.first.send("to_#{option}".to_sym) }
|
33
|
+
when Hash
|
34
|
+
selector, options = nil, args.first.to_operation_options
|
35
|
+
else
|
36
|
+
raise TypeError, ":to_operation_options returned an #{args.first.to_operation_options.class.name}. Must be an Array or Hash."
|
37
|
+
end
|
38
|
+
else
|
39
|
+
selector, options = nil, args.first || {} # 1 or 0 params
|
40
|
+
selector, options = options, {} unless options.is_a?(Hash) # swap if only selector provided
|
41
|
+
selector, options = args[0, 2] if args.many? # 2 or more params
|
42
|
+
options.stringify_keys!
|
43
|
+
options.each { |key, value| options[key] = value.send("to_#{key}".to_sym) if value.respond_to?("to_#{key}".to_sym) }
|
44
|
+
end
|
45
|
+
options["selector"] = selector if selector && options.exclude?("selector")
|
46
|
+
options["selector"] = previous_selector if previous_selector && options.exclude?("selector")
|
47
|
+
if options.include?("selector")
|
48
|
+
@previous_selector = options["selector"]
|
49
|
+
options["selector"] = identifiable?(previous_selector) ? dom_id(previous_selector) : previous_selector
|
50
|
+
end
|
51
|
+
@enqueued_operations[name.to_s] << options
|
52
|
+
self
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_json(*args)
|
57
|
+
@enqueued_operations.to_json(*args)
|
58
|
+
end
|
59
|
+
|
60
|
+
def apply!(operations = "{}")
|
61
|
+
operations = begin
|
62
|
+
JSON.parse(operations.is_a?(String) ? operations : operations.to_json)
|
63
|
+
rescue JSON::ParserError
|
64
|
+
{}
|
65
|
+
end
|
66
|
+
operations.each do |name, operation|
|
67
|
+
operation.each do |enqueued_operation|
|
68
|
+
@enqueued_operations[name.to_s] << enqueued_operation
|
69
|
+
end
|
70
|
+
end
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
def operations_payload
|
75
|
+
@enqueued_operations.select { |_, list| list.present? }.deep_transform_keys { |key| key.to_s.camelize(:lower) }
|
76
|
+
end
|
77
|
+
|
78
|
+
def reset!
|
79
|
+
@enqueued_operations = Hash.new { |hash, key| hash[key] = [] }
|
80
|
+
@previous_selector = nil
|
81
|
+
end
|
82
|
+
end
|
83
|
+
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 ENV["SKIP_SANITY_CHECK"]
|
11
|
+
return if CableReady.config.on_failed_sanity_checks == :ignore
|
12
|
+
return if called_by_generate_config?
|
13
|
+
return if called_by_rake?
|
14
|
+
|
15
|
+
instance = new
|
16
|
+
instance.check_package_versions_match
|
17
|
+
instance.check_new_version_available
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def called_by_generate_config?
|
23
|
+
ARGV.include?("cable_ready:initializer")
|
24
|
+
end
|
25
|
+
|
26
|
+
def called_by_rake?
|
27
|
+
File.basename($PROGRAM_NAME) == "rake"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def check_package_versions_match
|
32
|
+
if npm_version.nil?
|
33
|
+
warn_and_exit <<~WARN
|
34
|
+
👉 Can't locate the cable_ready npm package.
|
35
|
+
|
36
|
+
yarn add cable_ready@#{gem_version}
|
37
|
+
|
38
|
+
Either add it to your package.json as a dependency or use "yarn link cable_ready" if you are doing development.
|
39
|
+
WARN
|
40
|
+
end
|
41
|
+
|
42
|
+
if package_version_mismatch?
|
43
|
+
warn_and_exit <<~WARN
|
44
|
+
👉 The cable_ready npm package version (#{npm_version}) does not match the Rubygem version (#{gem_version}).
|
45
|
+
|
46
|
+
To update the cable_ready npm package:
|
47
|
+
|
48
|
+
yarn upgrade cable_ready@#{gem_version}
|
49
|
+
WARN
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def check_new_version_available
|
54
|
+
return if CableReady.config.on_new_version_available == :ignore
|
55
|
+
return if Rails.env.development? == false
|
56
|
+
return if using_preview_release?
|
57
|
+
begin
|
58
|
+
latest_version = URI.open("https://raw.githubusercontent.com/stimulusreflex/cable_ready/master/LATEST", open_timeout: 1, read_timeout: 1).read.strip
|
59
|
+
if latest_version != CableReady::VERSION
|
60
|
+
puts <<~WARN
|
61
|
+
|
62
|
+
👉 There is a new version of CableReady available!
|
63
|
+
Current: #{CableReady::VERSION} Latest: #{latest_version}
|
64
|
+
|
65
|
+
If you upgrade, it is very important that you update BOTH Gemfile and package.json
|
66
|
+
Then, run `bundle install && yarn install` to update to #{latest_version}.
|
67
|
+
|
68
|
+
WARN
|
69
|
+
exit if CableReady.config.on_new_version_available == :exit
|
70
|
+
end
|
71
|
+
rescue
|
72
|
+
puts "👉 CableReady #{CableReady::VERSION} update check skipped: connection timeout"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def package_version_mismatch?
|
79
|
+
npm_version != gem_version
|
80
|
+
end
|
81
|
+
|
82
|
+
def using_preview_release?
|
83
|
+
preview = CableReady::VERSION.match?(LATEST_VERSION_FORMAT) == false
|
84
|
+
puts "👉 CableReady #{CableReady::VERSION} update check skipped: pre-release build" if preview
|
85
|
+
preview
|
86
|
+
end
|
87
|
+
|
88
|
+
def gem_version
|
89
|
+
@_gem_version ||= CableReady::VERSION.gsub(".pre", "-pre")
|
90
|
+
end
|
91
|
+
|
92
|
+
def npm_version
|
93
|
+
@_npm_version ||= find_npm_version
|
94
|
+
end
|
95
|
+
|
96
|
+
def find_npm_version
|
97
|
+
if (match = search_file(package_json_path, regex: /version/))
|
98
|
+
match[JSON_VERSION_FORMAT, 1]
|
99
|
+
elsif (match = search_file(yarn_lock_path, regex: /^cable_ready/))
|
100
|
+
match[NODE_VERSION_FORMAT, 1]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def search_file(path, regex:)
|
105
|
+
return if File.exist?(path) == false
|
106
|
+
File.foreach(path).grep(regex).first
|
107
|
+
end
|
108
|
+
|
109
|
+
def package_json_path
|
110
|
+
Rails.root.join("node_modules", "cable_ready", "package.json")
|
111
|
+
end
|
112
|
+
|
113
|
+
def yarn_lock_path
|
114
|
+
Rails.root.join("yarn.lock")
|
115
|
+
end
|
116
|
+
|
117
|
+
def initializer_missing?
|
118
|
+
File.exist?(Rails.root.join("config", "initializers", "cable_ready.rb")) == false
|
119
|
+
end
|
120
|
+
|
121
|
+
def warn_and_exit(text)
|
122
|
+
puts
|
123
|
+
puts "Heads up! 🔥"
|
124
|
+
puts
|
125
|
+
puts text
|
126
|
+
puts
|
127
|
+
if CableReady.config.on_failed_sanity_checks == :exit
|
128
|
+
puts <<~INFO
|
129
|
+
To ignore any warnings and start the application anyway, you can set the SKIP_SANITY_CHECK environment variable:
|
130
|
+
|
131
|
+
SKIP_SANITY_CHECK=true rails
|
132
|
+
|
133
|
+
To do this permanently, add the following directive to the CableReady initializer:
|
134
|
+
|
135
|
+
CableReady.configure do |config|
|
136
|
+
config.on_failed_sanity_checks = :warn
|
137
|
+
end
|
138
|
+
|
139
|
+
INFO
|
140
|
+
if initializer_missing?
|
141
|
+
puts <<~INFO
|
142
|
+
You can create a CableReady initializer with the command:
|
143
|
+
|
144
|
+
bundle exec rails generate cable_ready:initializer
|
145
|
+
|
146
|
+
INFO
|
147
|
+
end
|
148
|
+
exit false if Rails.env.test? == false
|
149
|
+
end
|
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
|
data/lib/cable_ready/version.rb
CHANGED
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CableReady::ChannelGenerator < Rails::Generators::NamedBase
|
4
|
+
source_root File.expand_path("templates", __dir__)
|
5
|
+
|
6
|
+
class_option :stream_from, type: :string
|
7
|
+
class_option :stream_for, type: :string
|
8
|
+
class_option :stimulus, type: :boolean
|
9
|
+
|
10
|
+
def check_options
|
11
|
+
raise "Can't specify --stream-from and --stream-for at the same time" if options.key?(:stream_from) && options.key?(:stream_for)
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_channel
|
15
|
+
generate "channel", file_name
|
16
|
+
end
|
17
|
+
|
18
|
+
def enhance_channels
|
19
|
+
if using_broadcast_to?
|
20
|
+
gsub_file "app/channels/#{file_name}_channel.rb", /# stream_from.*\n/, "stream_for #{resource}.find(params[:id])\n"
|
21
|
+
template "app/javascript/controllers/%file_name%_controller.js" if using_stimulus?
|
22
|
+
else
|
23
|
+
prepend_to_file "app/javascript/channels/#{file_name}_channel.js", "import CableReady from 'cable_ready'\n"
|
24
|
+
inject_into_file "app/javascript/channels/#{file_name}_channel.js", after: "// Called when there's incoming data on the websocket for this channel\n" do
|
25
|
+
<<-JS
|
26
|
+
if (data.cableReady) CableReady.perform(data.operations)
|
27
|
+
JS
|
28
|
+
end
|
29
|
+
|
30
|
+
gsub_file "app/channels/#{file_name}_channel.rb", /# stream_from.*\n/, "stream_from \"#{identifier}\"\n"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def option_given?
|
37
|
+
options.key?(:stream_from) || options.key?(:stream_for)
|
38
|
+
end
|
39
|
+
|
40
|
+
def using_broadcast_to?
|
41
|
+
@using_broadcast_to ||= option_given? ? options.key?(:stream_for) : yes?("Are you streaming to a resource using broadcast_to? (y/N)")
|
42
|
+
end
|
43
|
+
|
44
|
+
def using_stimulus?
|
45
|
+
@using_stimulus ||= options.fetch(:stimulus) {
|
46
|
+
yes?("Are you going to use a Stimulus controller to subscribe to this channel? (y/N)")
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def resource
|
51
|
+
return @resource if @resource
|
52
|
+
|
53
|
+
stream_for = options.fetch(:stream_for) {
|
54
|
+
ask("Which resource are you streaming for?", default: class_name)
|
55
|
+
}
|
56
|
+
|
57
|
+
stream_for = file_name if stream_for == "stream_for"
|
58
|
+
@resource = stream_for.camelize
|
59
|
+
end
|
60
|
+
|
61
|
+
def identifier
|
62
|
+
return @identifier if @identifier
|
63
|
+
|
64
|
+
stream_from = options.fetch(:stream_from) {
|
65
|
+
ask("What is the stream identifier that goes into stream_from?", default: file_name)
|
66
|
+
}
|
67
|
+
|
68
|
+
stream_from = file_name if stream_from == "stream_from"
|
69
|
+
@identifier = stream_from.underscore
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators"
|
4
|
+
|
5
|
+
module CableReady
|
6
|
+
class InitializerGenerator < Rails::Generators::Base
|
7
|
+
desc "Creates a CableReady initializer template in config/initializers"
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
9
|
+
|
10
|
+
def copy_initializer_file
|
11
|
+
copy_file "config/initializers/cable_ready.rb"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators"
|
4
|
+
require "fileutils"
|
5
|
+
|
6
|
+
module CableReady
|
7
|
+
class StreamFromGenerator < Rails::Generators::Base
|
8
|
+
desc "Initializes CableReady with a reference to the shared ActionCable consumer"
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
10
|
+
|
11
|
+
def copy_controller_file
|
12
|
+
main_folder = defined?(Webpacker) ? Webpacker.config.source_path.to_s.gsub("#{Rails.root}/", "") : "app/javascript"
|
13
|
+
|
14
|
+
filepath = [
|
15
|
+
"#{main_folder}/controllers/index.js",
|
16
|
+
"#{main_folder}/controllers/index.ts",
|
17
|
+
"#{main_folder}/packs/application.js",
|
18
|
+
"#{main_folder}/packs/application.ts"
|
19
|
+
]
|
20
|
+
.select { |path| File.exist?(path) }
|
21
|
+
.map { |path| Rails.root.join(path) }
|
22
|
+
.first
|
23
|
+
|
24
|
+
lines = File.open(filepath, "r") { |f| f.readlines }
|
25
|
+
|
26
|
+
unless lines.find { |line| line.start_with?("import CableReady") }
|
27
|
+
matches = lines.select { |line| line =~ /\A(require|import)/ }
|
28
|
+
lines.insert lines.index(matches.last).to_i + 1, "import CableReady from 'cable_ready'\n"
|
29
|
+
File.open(filepath, "w") { |f| f.write lines.join }
|
30
|
+
end
|
31
|
+
|
32
|
+
unless lines.find { |line| line.start_with?("import consumer") }
|
33
|
+
matches = lines.select { |line| line =~ /\A(require|import)/ }
|
34
|
+
lines.insert lines.index(matches.last).to_i + 1, "import consumer from '../channels/consumer'\n"
|
35
|
+
File.open(filepath, "w") { |f| f.write lines.join }
|
36
|
+
end
|
37
|
+
|
38
|
+
unless lines.find { |line| line.include?("CableReady.initialize({ consumer })") }
|
39
|
+
append_to_file filepath, "CableReady.initialize({ consumer })"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
CableReady.configure do |config|
|
4
|
+
# Enable/disable exiting / warning when the sanity checks fail options:
|
5
|
+
# `:exit` or `:warn` or `:ignore`
|
6
|
+
|
7
|
+
# config.on_failed_sanity_checks = :exit
|
8
|
+
|
9
|
+
# Enable/disable exiting / warning when there's a new CableReady release
|
10
|
+
# `:exit` or `:warn` or `:ignore`
|
11
|
+
|
12
|
+
# config.on_new_version_available = :ignore
|
13
|
+
|
14
|
+
# Define your own custom operations
|
15
|
+
# https://cableready.stimulusreflex.com/customization#custom-operations
|
16
|
+
|
17
|
+
# config.add_operation_name :jazz_hands
|
18
|
+
end
|
data/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "cable_ready",
|
3
|
-
"version": "
|
3
|
+
"version": "5.0.0-pre1",
|
4
4
|
"description": "CableReady helps you create great real-time user experiences by making it simple to trigger client-side DOM changes from server-side Ruby.",
|
5
5
|
"keywords": [
|
6
6
|
"ruby",
|
@@ -15,17 +15,22 @@
|
|
15
15
|
],
|
16
16
|
"homepage": "https://cableready.stimulusreflex.com/",
|
17
17
|
"bugs": {
|
18
|
-
"url": "https://github.com/
|
18
|
+
"url": "https://github.com/stimulusreflex/cable_ready/issues"
|
19
19
|
},
|
20
20
|
"repository": {
|
21
21
|
"type": "git",
|
22
|
-
"url": "git+https://github.com:
|
22
|
+
"url": "git+https://github.com:stimulusreflex/cable_ready.git"
|
23
23
|
},
|
24
24
|
"license": "MIT",
|
25
25
|
"author": "Nathan Hopkins <natehop@gmail.com>",
|
26
|
-
"main": "./javascript/
|
26
|
+
"main": "./javascript/index.js",
|
27
|
+
"module": "./javascript/index.js",
|
28
|
+
"sideEffects": false,
|
27
29
|
"scripts": {
|
28
|
-
"
|
30
|
+
"lint": "yarn run prettier-standard:check",
|
31
|
+
"format": "yarn run prettier-standard:format",
|
32
|
+
"prettier-standard:check": "yarn run prettier-standard --check ./javascript/**/*.js",
|
33
|
+
"prettier-standard:format": "yarn run prettier-standard ./javascript/**/*.js"
|
29
34
|
},
|
30
35
|
"dependencies": {
|
31
36
|
"morphdom": "^2.6.1"
|