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.
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"