cable_ready 4.5.0 → 5.0.0.pre0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 dom_id(record, prefix = nil)
14
- "##{ActionView::RecordIdentifier.dom_id(record, prefix)}"
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
@@ -1,47 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CableReady
4
- class Channel
5
- attr_reader :identifier, :enqueued_operations
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" => broadcastable_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" => broadcastable_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 add_operation_method(name)
28
- return if respond_to?(name)
29
- singleton_class.public_send :define_method, name, ->(options = {}) {
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 broadcastable_operations
42
- enqueued_operations
43
- .select { |_, list| list.present? }
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
@@ -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 [](identifier)
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
 
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CableReady
4
+ module Compoundable
5
+ def compound(keys)
6
+ keys.map { |key|
7
+ key.class < ActiveRecord::Base ? key.to_global_id.to_s : key.to_s
8
+ }.join(":")
9
+ end
10
+ end
11
+ end
@@ -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