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