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,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