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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CableReadyHelper
4
+ include CableReady::Compoundable
5
+ include CableReady::StreamIdentifier
6
+
7
+ def stream_from(*keys)
8
+ keys.select!(&:itself)
9
+ tag.stream_from(identifier: signed_stream_identifier(compound(keys)))
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CableReadyBroadcastJob < (defined?(ActiveJob::Base) ? ActiveJob::Base : Object)
4
+ include CableReady::Broadcaster
5
+ queue_as :default if defined?(ActiveJob::Base)
6
+
7
+ def perform(identifier:, operations:, model: nil)
8
+ if model.present?
9
+ cable_ready[identifier.safe_constantize].apply!(operations).broadcast_to(model)
10
+ else
11
+ cable_ready[identifier].apply!(operations).broadcast
12
+ end
13
+ end
14
+ end
data/bin/standardize CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  bundle exec magic_frozen_string_literal
4
4
  bundle exec standardrb --fix
5
- yarn run prettier-standard ./javascript/*.js
5
+ yarn format
data/cable_ready.gemspec CHANGED
@@ -8,13 +8,14 @@ Gem::Specification.new do |gem|
8
8
  gem.version = CableReady::VERSION
9
9
  gem.authors = ["Nathan Hopkins"]
10
10
  gem.email = ["natehop@gmail.com"]
11
- gem.homepage = "https://github.com/hopsoft/cable_ready"
11
+ gem.homepage = "https://github.com/stimulusreflex/cable_ready"
12
12
  gem.summary = "Out-of-Band Server Triggered DOM Operations"
13
13
 
14
- gem.files = Dir["lib/**/*.rb", "app/assets/javascripts/cable_ready.js", "bin/*", "[A-Z]*"]
14
+ gem.files = Dir["lib/**/*.rb", "app/**/*.rb", "bin/*", "[A-Z]*"]
15
15
  gem.test_files = Dir["test/**/*.rb"]
16
16
 
17
17
  gem.add_dependency "rails", ">= 5.2"
18
+ gem.add_dependency "thread-local", ">= 1.1.0"
18
19
 
19
20
  gem.add_development_dependency "github_changelog_generator"
20
21
  gem.add_development_dependency "magic_frozen_string_literal"
@@ -22,4 +23,5 @@ Gem::Specification.new do |gem|
22
23
  gem.add_development_dependency "pry-nav"
23
24
  gem.add_development_dependency "rake"
24
25
  gem.add_development_dependency "standardrb"
26
+ gem.add_development_dependency "mocha"
25
27
  end
data/lib/cable_ready.rb CHANGED
@@ -1,11 +1,55 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails/engine"
4
+ require "open-uri"
5
+ require "active_record"
6
+ require "action_view"
4
7
  require "active_support/all"
8
+ require "thread/local"
9
+ require "monitor"
10
+ require "observer"
11
+ require "singleton"
5
12
  require "cable_ready/version"
13
+ require "cable_ready/identifiable"
14
+ require "cable_ready/operation_builder"
15
+ require "cable_ready/config"
6
16
  require "cable_ready/broadcaster"
17
+ require "cable_ready/sanity_checker"
18
+ require "cable_ready/compoundable"
19
+ require "cable_ready/channel"
20
+ require "cable_ready/channels"
21
+ require "cable_ready/cable_car"
22
+ require "cable_ready/stream_identifier"
7
23
 
8
24
  module CableReady
9
25
  class Engine < Rails::Engine
26
+ initializer "cable_ready.sanity_check" do
27
+ SanityChecker.check! unless Rails.env.production?
28
+ end
29
+
30
+ initializer "renderer" do
31
+ ActiveSupport.on_load(:action_controller) do
32
+ ActionController::Renderers.add :operations do |operations, options|
33
+ response.content_type ||= Mime[:cable_ready]
34
+ render json: operations.dispatch
35
+ end
36
+
37
+ Mime::Type.register "application/vnd.cable-ready.json", :cable_ready
38
+ end
39
+ end
40
+ end
41
+
42
+ class << self
43
+ def config
44
+ CableReady::Config.instance
45
+ end
46
+
47
+ def configure
48
+ yield config
49
+ end
50
+
51
+ def signed_stream_verifier
52
+ @signed_stream_verifier ||= ActiveSupport::MessageVerifier.new(config.verifier_key, digest: "SHA256", serializer: JSON)
53
+ end
10
54
  end
11
55
  end
@@ -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,49 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CableReady
4
- class Channel
5
- attr_reader :identifier, :operations, :available_operations
4
+ class Channel < OperationBuilder
5
+ attr_reader :identifier
6
6
 
7
- def initialize(identifier, available_operations)
8
- @identifier = identifier
9
- @available_operations = available_operations
10
- reset
11
- available_operations.each do |available_operation, implementation|
12
- define_singleton_method available_operation, &implementation
13
- end
7
+ def broadcast(clear: true)
8
+ ActionCable.server.broadcast identifier, {"cableReady" => true, "operations" => operations_payload}
9
+ reset! if clear
14
10
  end
15
11
 
16
- def channel_broadcast(clear)
17
- operations.select! { |_, list| list.present? }
18
- operations.deep_transform_keys! { |key| key.to_s.camelize(:lower) }
19
- ActionCable.server.broadcast identifier, {"cableReady" => true, "operations" => operations}
20
- reset if clear
12
+ def broadcast_to(model, clear: true)
13
+ identifier.broadcast_to model, {"cableReady" => true, "operations" => operations_payload}
14
+ reset! if clear
21
15
  end
22
16
 
23
- def channel_broadcast_to(model, clear)
24
- operations.select! { |_, list| list.present? }
25
- operations.deep_transform_keys! { |key| key.to_s.camelize(:lower) }
26
- identifier.broadcast_to model, {"cableReady" => true, "operations" => operations}
27
- reset if clear
17
+ def broadcast_later(clear: true)
18
+ CableReadyBroadcastJob.perform_later(identifier: identifier, operations: operations_payload)
19
+ reset! if clear
28
20
  end
29
21
 
30
- def broadcast(clear = true)
31
- CableReady::Channels.instance.broadcast(identifier, clear: clear)
32
- end
33
-
34
- def broadcast_to(model, clear = true)
35
- CableReady::Channels.instance.broadcast_to(model, identifier, clear: clear)
36
- end
37
-
38
- private
39
-
40
- def enqueue_operation(key, options)
41
- operations[key] << options
42
- self
43
- end
44
-
45
- def reset
46
- @operations = Hash.new { |hash, operation| hash[operation] = [] }
22
+ def broadcast_later_to(model, clear: true)
23
+ CableReadyBroadcastJob.perform_later(identifier: identifier.name, operations: operations_payload, model: model)
24
+ reset! if clear
47
25
  end
48
26
  end
49
27
  end
@@ -1,86 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "channel"
4
-
5
3
  module CableReady
4
+ # This class is a thread local singleton: CableReady::Channels.instance
5
+ # SEE: https://github.com/socketry/thread-local/tree/master/guides/getting-started
6
6
  class Channels
7
- include Singleton
8
- attr_accessor :operations
9
-
10
- def self.configure
11
- yield CableReady::Channels.instance if block_given?
12
- end
7
+ include Compoundable
8
+ extend Thread::Local
13
9
 
14
10
  def initialize
15
11
  @channels = {}
16
- @operations = {}
17
- %i[
18
- add_css_class
19
- clear_storage
20
- console_log
21
- dispatch_event
22
- inner_html
23
- insert_adjacent_html
24
- insert_adjacent_text
25
- morph
26
- notification
27
- outer_html
28
- push_state
29
- remove
30
- remove_attribute
31
- remove_css_class
32
- remove_storage_item
33
- set_attribute
34
- set_cookie
35
- set_dataset_property
36
- set_focus
37
- set_property
38
- set_storage_item
39
- set_style
40
- set_styles
41
- set_value
42
- text_content
43
- ].each do |operation|
44
- add_operation operation
45
- end
46
- end
47
-
48
- def add_operation(operation, &implementation)
49
- @operations[operation] = implementation || ->(options = {}) { enqueue_operation(operation, options) }
50
12
  end
51
13
 
52
- def [](identifier)
53
- @channels[identifier] ||= CableReady::Channel.new(identifier, operations)
14
+ def [](*keys)
15
+ keys.select!(&:itself)
16
+ identifier = keys.many? || (keys.one? && keys.first.is_a?(ActiveRecord::Base)) ? compound(keys) : keys.pop
17
+ @channels[identifier] ||= CableReady::Channel.new(identifier)
54
18
  end
55
19
 
56
20
  def broadcast(*identifiers, clear: true)
57
- mutex.synchronize do
58
- @channels.values
59
- .reject { |channel| identifiers.any? && identifiers.exclude?(channel.identifier) }
60
- .select { |channel| channel.identifier.is_a?(String) }
61
- .tap do |channels|
62
- channels.each { |channel| @channels[channel.identifier].channel_broadcast(clear) }
63
- channels.each { |channel| @channels.except!(channel.identifier) if clear }
64
- end
65
- end
21
+ @channels.values
22
+ .reject { |channel| identifiers.any? && identifiers.exclude?(channel.identifier) }
23
+ .select { |channel| channel.identifier.is_a?(String) }
24
+ .tap do |channels|
25
+ channels.each { |channel| @channels[channel.identifier].broadcast(clear: clear) }
26
+ channels.each { |channel| @channels.except!(channel.identifier) if clear }
27
+ end
66
28
  end
67
29
 
68
30
  def broadcast_to(model, *identifiers, clear: true)
69
- mutex.synchronize do
70
- @channels.values
71
- .reject { |channel| identifiers.any? && identifiers.exclude?(channel.identifier) }
72
- .reject { |channel| channel.identifier.is_a?(String) }
73
- .tap do |channels|
74
- channels.each { |channel| @channels[channel.identifier].channel_broadcast_to(model, clear) }
75
- channels.each { |channel| @channels.except!(channel.identifier) if clear }
76
- end
77
- end
78
- end
79
-
80
- private
81
-
82
- def mutex
83
- @mutex ||= Mutex.new
31
+ @channels.values
32
+ .reject { |channel| identifiers.any? && identifiers.exclude?(channel.identifier) }
33
+ .reject { |channel| channel.identifier.is_a?(String) }
34
+ .tap do |channels|
35
+ channels.each { |channel| @channels[channel.identifier].broadcast_to(model, clear: clear) }
36
+ channels.each { |channel| @channels.except!(channel.identifier) if clear }
37
+ end
84
38
  end
85
39
  end
86
40
  end
@@ -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
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CableReady
4
+ # This class is a process level singleton shared by all threads: CableReady::Config.instance
5
+ class Config
6
+ include MonitorMixin
7
+ include Observable
8
+ include Singleton
9
+
10
+ attr_accessor :on_failed_sanity_checks, :on_new_version_available
11
+ attr_writer :verifier_key
12
+
13
+ def initialize
14
+ super
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")
26
+ end
27
+
28
+ def operation_names
29
+ @operation_names.to_a
30
+ end
31
+
32
+ def add_operation_name(name)
33
+ synchronize do
34
+ @operation_names << name.to_sym
35
+ notify_observers name.to_sym
36
+ end
37
+ end
38
+
39
+ def default_operation_names
40
+ Set.new(%i[
41
+ add_css_class
42
+ append
43
+ clear_storage
44
+ console_log
45
+ console_table
46
+ dispatch_event
47
+ go
48
+ graft
49
+ inner_html
50
+ insert_adjacent_html
51
+ insert_adjacent_text
52
+ morph
53
+ notification
54
+ outer_html
55
+ prepend
56
+ push_state
57
+ remove
58
+ remove_attribute
59
+ remove_css_class
60
+ remove_storage_item
61
+ replace
62
+ replace_state
63
+ scroll_into_view
64
+ set_attribute
65
+ set_cookie
66
+ set_dataset_property
67
+ set_focus
68
+ set_meta
69
+ set_property
70
+ set_storage_item
71
+ set_style
72
+ set_styles
73
+ set_value
74
+ text_content
75
+ ]).freeze
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CableReady
4
+ module Identifiable
5
+ def dom_id(record, prefix = nil)
6
+ return record.to_dom_selector if record.respond_to?(:to_dom_selector)
7
+
8
+ prefix = prefix.to_s.strip if prefix
9
+
10
+ id = if record.respond_to?(:to_dom_id)
11
+ record.to_dom_id
12
+ elsif record.is_a?(ActiveRecord::Relation)
13
+ [prefix, record.model_name.plural].compact.join("_")
14
+ elsif record.is_a?(ActiveRecord::Base)
15
+ ActionView::RecordIdentifier.dom_id(record, prefix)
16
+ else
17
+ [prefix, record.to_s.strip].compact.join("_")
18
+ end
19
+
20
+ "##{id}".squeeze("#").strip.downcase
21
+ end
22
+
23
+ def identifiable?(obj)
24
+ obj.respond_to?(:to_dom_selector) ||
25
+ obj.respond_to?(:to_dom_id) ||
26
+ obj.is_a?(ActiveRecord::Relation) ||
27
+ obj.is_a?(ActiveRecord::Base)
28
+ end
29
+ end
30
+ end