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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +228 -161
  3. data/Gemfile.lock +146 -99
  4. data/LATEST +1 -0
  5. data/README.md +13 -13
  6. data/Rakefile +8 -2
  7. data/app/channels/cable_ready/stream.rb +12 -0
  8. data/app/helpers/cable_ready_helper.rb +11 -0
  9. data/app/jobs/cable_ready_broadcast_job.rb +14 -0
  10. data/bin/standardize +1 -1
  11. data/cable_ready.gemspec +4 -2
  12. data/lib/cable_ready.rb +44 -0
  13. data/lib/cable_ready/broadcaster.rb +3 -4
  14. data/lib/cable_ready/cable_car.rb +17 -0
  15. data/lib/cable_ready/channel.rb +14 -36
  16. data/lib/cable_ready/channels.rb +22 -68
  17. data/lib/cable_ready/compoundable.rb +11 -0
  18. data/lib/cable_ready/config.rb +78 -0
  19. data/lib/cable_ready/identifiable.rb +30 -0
  20. data/lib/cable_ready/operation_builder.rb +83 -0
  21. data/lib/cable_ready/sanity_checker.rb +151 -0
  22. data/lib/cable_ready/stream_identifier.rb +13 -0
  23. data/lib/cable_ready/version.rb +1 -1
  24. data/lib/generators/cable_ready/channel_generator.rb +71 -0
  25. data/lib/generators/cable_ready/initializer_generator.rb +14 -0
  26. data/lib/generators/cable_ready/stream_from_generator.rb +43 -0
  27. data/lib/generators/cable_ready/templates/config/initializers/cable_ready.rb +18 -0
  28. data/package.json +10 -5
  29. data/tags +58 -35
  30. data/test/lib/cable_ready/cable_car_test.rb +50 -0
  31. data/test/lib/cable_ready/identifiable_test.rb +75 -0
  32. data/test/lib/cable_ready/operation_builder_test.rb +211 -0
  33. data/test/lib/generators/cable_ready/channel_generator_test.rb +157 -0
  34. data/test/support/generator_test_helpers.rb +28 -0
  35. data/test/test_helper.rb +15 -0
  36. data/yarn.lock +134 -124
  37. 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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CableReady
4
- VERSION = "4.4.6"
4
+ VERSION = "5.0.0.pre2"
5
5
  end
@@ -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": "4.4.5",
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/hopsoft/cable_ready/issues"
18
+ "url": "https://github.com/stimulusreflex/cable_ready/issues"
19
19
  },
20
20
  "repository": {
21
21
  "type": "git",
22
- "url": "git+https://github.com:hopsoft/cable_ready.git"
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/cable_ready.js",
26
+ "main": "./javascript/index.js",
27
+ "module": "./javascript/index.js",
28
+ "sideEffects": false,
27
29
  "scripts": {
28
- "prettier-standard-check": "yarn run prettier-standard --check ./javascript/**/*.js"
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"