From 759132ade28ffc162bff328303e45d930ec3a135 Mon Sep 17 00:00:00 2001 From: Hopsoft Date: Fri, 8 Jan 2021 14:04:24 -0700 Subject: [PATCH 01/10] Global config, simplify threading, custom operations --- Gemfile.lock | 150 ++++++++++++++++--------------- cable_ready.gemspec | 1 + lib/cable_ready.rb | 10 +++ lib/cable_ready/channel.rb | 50 +++++------ lib/cable_ready/channels.rb | 86 +++++------------- lib/cable_ready/config.rb | 67 ++++++++++++++ lib/cable_ready/configuration.rb | 7 ++ 7 files changed, 208 insertions(+), 163 deletions(-) create mode 100644 lib/cable_ready/config.rb create mode 100644 lib/cable_ready/configuration.rb diff --git a/Gemfile.lock b/Gemfile.lock index 1277a987..ab46368a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,64 +3,65 @@ PATH specs: cable_ready (4.4.6) rails (>= 5.2) + thread-local (>= 1.1.0) GEM remote: https://rubygems.org/ specs: - actioncable (6.1.0) - actionpack (= 6.1.0) - activesupport (= 6.1.0) + actioncable (6.1.1) + actionpack (= 6.1.1) + activesupport (= 6.1.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.0) - actionpack (= 6.1.0) - activejob (= 6.1.0) - activerecord (= 6.1.0) - activestorage (= 6.1.0) - activesupport (= 6.1.0) + actionmailbox (6.1.1) + actionpack (= 6.1.1) + activejob (= 6.1.1) + activerecord (= 6.1.1) + activestorage (= 6.1.1) + activesupport (= 6.1.1) mail (>= 2.7.1) - actionmailer (6.1.0) - actionpack (= 6.1.0) - actionview (= 6.1.0) - activejob (= 6.1.0) - activesupport (= 6.1.0) + actionmailer (6.1.1) + actionpack (= 6.1.1) + actionview (= 6.1.1) + activejob (= 6.1.1) + activesupport (= 6.1.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.0) - actionview (= 6.1.0) - activesupport (= 6.1.0) + actionpack (6.1.1) + actionview (= 6.1.1) + activesupport (= 6.1.1) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.0) - actionpack (= 6.1.0) - activerecord (= 6.1.0) - activestorage (= 6.1.0) - activesupport (= 6.1.0) + actiontext (6.1.1) + actionpack (= 6.1.1) + activerecord (= 6.1.1) + activestorage (= 6.1.1) + activesupport (= 6.1.1) nokogiri (>= 1.8.5) - actionview (6.1.0) - activesupport (= 6.1.0) + actionview (6.1.1) + activesupport (= 6.1.1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.1.0) - activesupport (= 6.1.0) + activejob (6.1.1) + activesupport (= 6.1.1) globalid (>= 0.3.6) - activemodel (6.1.0) - activesupport (= 6.1.0) - activerecord (6.1.0) - activemodel (= 6.1.0) - activesupport (= 6.1.0) - activestorage (6.1.0) - actionpack (= 6.1.0) - activejob (= 6.1.0) - activerecord (= 6.1.0) - activesupport (= 6.1.0) + activemodel (6.1.1) + activesupport (= 6.1.1) + activerecord (6.1.1) + activemodel (= 6.1.1) + activesupport (= 6.1.1) + activestorage (6.1.1) + actionpack (= 6.1.1) + activejob (= 6.1.1) + activerecord (= 6.1.1) + activesupport (= 6.1.1) marcel (~> 0.3.1) mimemagic (~> 0.3.2) - activesupport (6.1.0) + activesupport (6.1.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -74,11 +75,13 @@ GEM concurrent-ruby (1.1.7) crass (1.0.6) erubi (1.10.0) - faraday (1.1.0) + faraday (1.3.0) + faraday-net_http (~> 1.0) multipart-post (>= 1.2, < 3) ruby2_keywords faraday-http-cache (2.2.0) faraday (>= 0.8) + faraday-net_http (1.0.0) github_changelog_generator (1.15.2) activesupport faraday-http-cache @@ -89,7 +92,7 @@ GEM retriable (~> 3.0) globalid (0.4.2) activesupport (>= 4.2.0) - i18n (1.8.5) + i18n (1.8.7) concurrent-ruby (~> 1.0) loofah (2.8.0) crass (~> 1.0.2) @@ -102,18 +105,19 @@ GEM method_source (0.9.2) mimemagic (0.3.5) mini_mime (1.0.2) - mini_portile2 (2.4.0) - minitest (5.14.2) + mini_portile2 (2.5.0) + minitest (5.14.3) multi_json (1.15.0) multipart-post (2.1.1) nio4r (2.5.4) - nokogiri (1.10.10) - mini_portile2 (~> 2.4.0) - octokit (4.19.0) + nokogiri (1.11.1) + mini_portile2 (~> 2.5.0) + racc (~> 1.4) + octokit (4.20.0) faraday (>= 0.9) sawyer (~> 0.8.0, >= 0.5.3) parallel (1.20.1) - parser (2.7.2.0) + parser (3.0.0.0) ast (~> 2.4.1) pry (0.12.2) coderay (~> 1.1.0) @@ -121,55 +125,56 @@ GEM pry-nav (0.3.0) pry (>= 0.9.10, < 0.13.0) public_suffix (4.0.6) + racc (1.5.2) rack (2.2.3) rack-test (1.1.0) rack (>= 1.0, < 3) - rails (6.1.0) - actioncable (= 6.1.0) - actionmailbox (= 6.1.0) - actionmailer (= 6.1.0) - actionpack (= 6.1.0) - actiontext (= 6.1.0) - actionview (= 6.1.0) - activejob (= 6.1.0) - activemodel (= 6.1.0) - activerecord (= 6.1.0) - activestorage (= 6.1.0) - activesupport (= 6.1.0) + rails (6.1.1) + actioncable (= 6.1.1) + actionmailbox (= 6.1.1) + actionmailer (= 6.1.1) + actionpack (= 6.1.1) + actiontext (= 6.1.1) + actionview (= 6.1.1) + activejob (= 6.1.1) + activemodel (= 6.1.1) + activerecord (= 6.1.1) + activestorage (= 6.1.1) + activesupport (= 6.1.1) bundler (>= 1.15.0) - railties (= 6.1.0) + railties (= 6.1.1) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) rails-html-sanitizer (1.3.0) loofah (~> 2.3) - railties (6.1.0) - actionpack (= 6.1.0) - activesupport (= 6.1.0) + railties (6.1.1) + actionpack (= 6.1.1) + activesupport (= 6.1.1) method_source rake (>= 0.8.7) thor (~> 1.0) rainbow (3.0.0) - rake (13.0.1) - regexp_parser (2.0.0) + rake (13.0.3) + regexp_parser (2.0.3) retriable (3.1.2) rexml (3.2.4) - rubocop (1.4.2) + rubocop (1.7.0) parallel (~> 1.10) parser (>= 2.7.1.5) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8) + regexp_parser (>= 1.8, < 3.0) rexml - rubocop-ast (>= 1.1.1) + rubocop-ast (>= 1.2.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 2.0) - rubocop-ast (1.3.0) + rubocop-ast (1.4.0) parser (>= 2.7.1.5) - rubocop-performance (1.9.1) + rubocop-performance (1.9.2) rubocop (>= 0.90.0, < 2.0) rubocop-ast (>= 0.4.0) - ruby-progressbar (1.10.1) + ruby-progressbar (1.11.0) ruby2_keywords (0.0.2) sawyer (0.8.2) addressable (>= 2.3.5) @@ -181,12 +186,13 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - standard (0.10.2) - rubocop (= 1.4.2) - rubocop-performance (= 1.9.1) + standard (0.11.0) + rubocop (= 1.7.0) + rubocop-performance (= 1.9.2) standardrb (1.0.0) standard thor (1.0.1) + thread-local (1.1.0) tzinfo (2.0.4) concurrent-ruby (~> 1.0) unicode-display_width (1.7.0) diff --git a/cable_ready.gemspec b/cable_ready.gemspec index 8af8aba2..1703f3a5 100644 --- a/cable_ready.gemspec +++ b/cable_ready.gemspec @@ -15,6 +15,7 @@ Gem::Specification.new do |gem| gem.test_files = Dir["test/**/*.rb"] gem.add_dependency "rails", ">= 5.2" + gem.add_dependency "thread-local", ">= 1.1.0" gem.add_development_dependency "github_changelog_generator" gem.add_development_dependency "magic_frozen_string_literal" diff --git a/lib/cable_ready.rb b/lib/cable_ready.rb index 4321cf9a..3268671c 100644 --- a/lib/cable_ready.rb +++ b/lib/cable_ready.rb @@ -1,11 +1,21 @@ # frozen_string_literal: true +require "thread/local" require "rails/engine" require "active_support/all" require "cable_ready/version" +require "cable_ready/config" require "cable_ready/broadcaster" module CableReady class Engine < Rails::Engine end + + def self.config + CableReady::Config.instance + end + + def self.configure + yield config + end end diff --git a/lib/cable_ready/channel.rb b/lib/cable_ready/channel.rb index 71156b72..0e636acc 100644 --- a/lib/cable_ready/channel.rb +++ b/lib/cable_ready/channel.rb @@ -2,48 +2,48 @@ module CableReady class Channel - attr_reader :identifier, :operations, :available_operations + attr_reader :identifier, :enqueued_operations + alias_method :operations, :enqueued_operations # for backwards compatibility TODO: deprecate - def initialize(identifier, available_operations) + def initialize(identifier) @identifier = identifier - @available_operations = available_operations reset - available_operations.each do |available_operation, implementation| - define_singleton_method available_operation, &implementation - end - end + CableReady.config.operation_names.each { |name| add_operation_method name } - def channel_broadcast(clear) - operations.select! { |_, list| list.present? } - operations.deep_transform_keys! { |key| key.to_s.camelize(:lower) } - ActionCable.server.broadcast identifier, {"cableReady" => true, "operations" => operations} - reset if clear + config_observer = self + CableReady.config.add_observer config_observer, :add_operation_method + ObjectSpace.define_finalizer self, -> { CableReady.config.delete_observer config_observer } end - def channel_broadcast_to(model, clear) - operations.select! { |_, list| list.present? } - operations.deep_transform_keys! { |key| key.to_s.camelize(:lower) } - identifier.broadcast_to model, {"cableReady" => true, "operations" => operations} + def broadcast(clear: true) + ActionCable.server.broadcast identifier, {"cableReady" => true, "operations" => broadcastable_operations} reset if clear end + alias_method :channel_broadcast, :broadcast # for backwards compatibility TODO: deprecate - def broadcast(clear = true) - CableReady::Channels.instance.broadcast(identifier, clear: clear) + def broadcast_to(model, clear: true) + identifier.broadcast_to model, {"cableReady" => true, "operations" => broadcastable_operations} + reset if clear end + alias_method :channel_broadcast_to, :broadcast_to # for backwards compatibility TODO: deprecate - def broadcast_to(model, clear = true) - CableReady::Channels.instance.broadcast_to(model, identifier, clear: clear) + def add_operation_method(name) + return if respond_to?(name) + singleton_class.public_send :define_method, name, ->(options = {}) { + enqueued_operations[name.to_s] << options.stringify_keys + } end private - def enqueue_operation(key, options) - operations[key] << options - self + def reset + @enqueued_operations = Hash.new { |hash, key| hash[key] = [] } end - def reset - @operations = Hash.new { |hash, operation| hash[operation] = [] } + def broadcastable_operations + enqueued_operations + .select { |_, list| list.present? } + .deep_transform_keys! { |key| key.to_s.camelize(:lower) } end end end diff --git a/lib/cable_ready/channels.rb b/lib/cable_ready/channels.rb index 804f05af..69c1b7ad 100644 --- a/lib/cable_ready/channels.rb +++ b/lib/cable_ready/channels.rb @@ -1,89 +1,43 @@ # frozen_string_literal: true +require "thread/local" require_relative "channel" module CableReady + # This class is a thread local singleton: CableReady::Channels.instance + # SEE: https://github.com/socketry/thread-local/tree/master/guides/getting-started class Channels - include Singleton - attr_accessor :operations + extend Thread::Local - def self.configure - yield CableReady::Channels.instance if block_given? - end + attr_accessor :operations def initialize @channels = {} @operations = {} - %i[ - add_css_class - clear_storage - console_log - dispatch_event - inner_html - insert_adjacent_html - insert_adjacent_text - morph - notification - outer_html - push_state - remove - remove_attribute - remove_css_class - remove_storage_item - set_attribute - set_cookie - set_dataset_property - set_focus - set_property - set_storage_item - set_style - set_styles - set_value - text_content - ].each do |operation| - add_operation operation - end - end - - def add_operation(operation) - @operations[operation] = ->(options = {}) do - yield(options) if block_given? - enqueue_operation(operation, options) - end end def [](identifier) - @channels[identifier] ||= CableReady::Channel.new(identifier, operations) + @channels[identifier] ||= CableReady::Channel.new(identifier) end def broadcast(*identifiers, clear: true) - mutex.synchronize do - @channels.values - .reject { |channel| identifiers.any? && identifiers.exclude?(channel.identifier) } - .select { |channel| channel.identifier.is_a?(String) } - .tap do |channels| - channels.each { |channel| @channels[channel.identifier].channel_broadcast(clear) } - channels.each { |channel| @channels.except!(channel.identifier) if clear } - end - end + @channels.values + .reject { |channel| identifiers.any? && identifiers.exclude?(channel.identifier) } + .select { |channel| channel.identifier.is_a?(String) } + .tap do |channels| + channels.each { |channel| @channels[channel.identifier].broadcast(clear: clear) } + channels.each { |channel| @channels.except!(channel.identifier) if clear } + end end def broadcast_to(model, *identifiers, clear: true) - mutex.synchronize do - @channels.values - .reject { |channel| identifiers.any? && identifiers.exclude?(channel.identifier) } - .reject { |channel| channel.identifier.is_a?(String) } - .tap do |channels| - channels.each { |channel| @channels[channel.identifier].channel_broadcast_to(model, clear) } - channels.each { |channel| @channels.except!(channel.identifier) if clear } - end - end - end - - private - - def mutex - @mutex ||= Mutex.new + @channels.values + .reject { |channel| identifiers.any? && identifiers.exclude?(channel.identifier) } + .reject { |channel| channel.identifier.is_a?(String) } + .tap do |channels| + channels.each { |channel| @channels[channel.identifier].broadcast_to(model, clear: clear) } + channels.each { |channel| @channels.except!(channel.identifier) if clear } + end end end end diff --git a/lib/cable_ready/config.rb b/lib/cable_ready/config.rb new file mode 100644 index 00000000..88b4a74f --- /dev/null +++ b/lib/cable_ready/config.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require "monitor" +require "observer" +require "singleton" + +module CableReady + # This class is a process level singleton shared by all threads: CableReady::Config.instance + class Config + include Singleton + include Observable + + def initialize + @lock = Monitor.new + @operation_definitions = {} + default_operation_names.each { |name| add_operation_definition name } + end + + def operation_names + operation_definitions.keys + end + + # TODO: NATE: what are we doing with the passed options??? + # perhaps we can omit and simply have a list of operation names? + def add_operation_definition(name, options = {}) + @lock.synchronize do + yield options if block_given? # TODO: NATE: what are we doing with these options??? + operation_definitions[name.to_sym] = options # TODO: NATE: what are we doing with these options??? + notify_observers name.to_sym + end + end + + def default_operation_names + %i[ + add_css_class + clear_storage + console_log + dispatch_event + inner_html + insert_adjacent_html + insert_adjacent_text + morph + notification + outer_html + push_state + remove + remove_attribute + remove_css_class + remove_storage_item + set_attribute + set_cookie + set_dataset_property + set_focus + set_property + set_storage_item + set_style + set_styles + set_value + text_content + ].freeze + end + + private + + attr_reader :operation_definitions + end +end diff --git a/lib/cable_ready/configuration.rb b/lib/cable_ready/configuration.rb new file mode 100644 index 00000000..dce05318 --- /dev/null +++ b/lib/cable_ready/configuration.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module CableReady + class configuration + include Singleton + end +end From 45d24e0c644192350d11002fe20ac3b0db84aaca Mon Sep 17 00:00:00 2001 From: Hopsoft Date: Fri, 8 Jan 2021 14:10:07 -0700 Subject: [PATCH 02/10] Remove unused artifact --- lib/cable_ready/configuration.rb | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 lib/cable_ready/configuration.rb diff --git a/lib/cable_ready/configuration.rb b/lib/cable_ready/configuration.rb deleted file mode 100644 index dce05318..00000000 --- a/lib/cable_ready/configuration.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -module CableReady - class configuration - include Singleton - end -end From 18294dd48b8781023dd59ea1117b188da08c8b38 Mon Sep 17 00:00:00 2001 From: Hopsoft Date: Fri, 8 Jan 2021 14:11:53 -0700 Subject: [PATCH 03/10] Remove redundant require --- lib/cable_ready.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/cable_ready.rb b/lib/cable_ready.rb index 3268671c..750b6f6c 100644 --- a/lib/cable_ready.rb +++ b/lib/cable_ready.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require "thread/local" require "rails/engine" require "active_support/all" require "cable_ready/version" From e64a84a56da8c96311c9c2ca271922301479d44f Mon Sep 17 00:00:00 2001 From: leastbad Date: Sun, 10 Jan 2021 05:29:12 +0000 Subject: [PATCH 04/10] GitBook: [master] one page modified --- docs/leveraging-stimulus.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/leveraging-stimulus.md b/docs/leveraging-stimulus.md index c5bfd6d5..9f51f799 100644 --- a/docs/leveraging-stimulus.md +++ b/docs/leveraging-stimulus.md @@ -245,6 +245,10 @@ One question that comes up often on Discord is how to properly handle broadcasts Our proposed solution is that instead of modifying the DOM directly, send a `dispatch_event` that has the rendered HTML for **both** the current user and everyone else available in _different keys_ of the `detail` object, along with the `user_id` of the contributing user. This `user_id` can be compared against the current user's `id` which has already been stored in a `meta` tag in the document `head`. Upon receiving a new update, the Stimulus controller can append the correct HTML fragment to the correct DOM element and the project is saved. +{% hint style="warning" %} +This technique is not well-suited to scenarios where sensitive data is being transmitted. Since all data being sent is visible via Network Inspector, please assume that all everyone receiving a message can see its contents. +{% endhint %} + #### Example 4: The Stimulus value attribute setter The recent release of Stimulus v2 finally brought the [Values](https://stimulus.hotwire.dev/reference/values) API. Values maps a data attribute on the DOM element which holds the controller instance to a typed internal value. Updating the data attribute on the DOM element automatically fires a `ValueChanged` callback, if one is available. 👍 From 5f9d64480e91495986e53645cdd6fa2091917361 Mon Sep 17 00:00:00 2001 From: Hopsoft Date: Sun, 10 Jan 2021 09:29:36 -0700 Subject: [PATCH 05/10] Support chaining and rip deprecations out --- lib/cable_ready/channel.rb | 6 +++--- lib/cable_ready/config.rb | 20 ++++++-------------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/lib/cable_ready/channel.rb b/lib/cable_ready/channel.rb index 0e636acc..cec1650a 100644 --- a/lib/cable_ready/channel.rb +++ b/lib/cable_ready/channel.rb @@ -3,7 +3,6 @@ module CableReady class Channel attr_reader :identifier, :enqueued_operations - alias_method :operations, :enqueued_operations # for backwards compatibility TODO: deprecate def initialize(identifier) @identifier = identifier @@ -18,19 +17,20 @@ def initialize(identifier) def broadcast(clear: true) ActionCable.server.broadcast identifier, {"cableReady" => true, "operations" => broadcastable_operations} reset if clear + self end - alias_method :channel_broadcast, :broadcast # for backwards compatibility TODO: deprecate def broadcast_to(model, clear: true) identifier.broadcast_to model, {"cableReady" => true, "operations" => broadcastable_operations} reset if clear + self end - alias_method :channel_broadcast_to, :broadcast_to # for backwards compatibility TODO: deprecate def add_operation_method(name) return if respond_to?(name) singleton_class.public_send :define_method, name, ->(options = {}) { enqueued_operations[name.to_s] << options.stringify_keys + self # supports operation chaining } end diff --git a/lib/cable_ready/config.rb b/lib/cable_ready/config.rb index 88b4a74f..7a3ebda3 100644 --- a/lib/cable_ready/config.rb +++ b/lib/cable_ready/config.rb @@ -12,26 +12,22 @@ class Config def initialize @lock = Monitor.new - @operation_definitions = {} - default_operation_names.each { |name| add_operation_definition name } + @operation_names = Set.new(default_operation_names) end def operation_names - operation_definitions.keys + @operation_names.to_a end - # TODO: NATE: what are we doing with the passed options??? - # perhaps we can omit and simply have a list of operation names? - def add_operation_definition(name, options = {}) + def add_operation_name(name) @lock.synchronize do - yield options if block_given? # TODO: NATE: what are we doing with these options??? - operation_definitions[name.to_sym] = options # TODO: NATE: what are we doing with these options??? + @operation_names << name.to_sym notify_observers name.to_sym end end def default_operation_names - %i[ + Set.new(%i[ add_css_class clear_storage console_log @@ -57,11 +53,7 @@ def default_operation_names set_styles set_value text_content - ].freeze + ]).freeze end - - private - - attr_reader :operation_definitions end end From e58bb3ade73bdb83c0661e419b465112c5a2501a Mon Sep 17 00:00:00 2001 From: leastbad Date: Mon, 11 Jan 2021 02:11:24 +0000 Subject: [PATCH 06/10] GitBook: [master] one page modified --- docs/usage.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 4949d747..f21fdf06 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -123,8 +123,6 @@ In general, it's easier to track related concepts transactionally in one broadca CableReady does not \(and could not\) wait for event listeners to finish running before launching the primary function of any particular operation. This means that **if you do something slow in your event handler callback, it will likely finish running** _**after**_ **the operation is completed**. Depending on your expectations, this could cause chaos for inexperienced developers. -## - ## Focus assignment The [DOM Mutation](reference/operations/dom-mutations.md) operations accept an optional `focusSelector` parameter that allows you to specify a CSS selector to which element should be active \(receive focus\) after the operation completes. @@ -155,6 +153,22 @@ ActionCable.server.broadcast("users:#{current_user.to_gid_param}", data) `UsersChannel` becomes `users` while ActiveRecord has a `to_gid_param`. +## Poking a subscriber + +Sometimes you just need to tell a subscriber that it's time to _do the thing_. You can send a `broadcast` with no operations and still take advantage of the `received` handler: + +```ruby +cable_ready["stream"].broadcast +``` + +```javascript +consumer.subscriptions.create('ChewiesChannel', { + received (data) { + console.log('Data was received!') + } +}) +``` + ## Disconnect a user from their ActionCable Connection As you can see in the upcoming section on [connection identifiers](identifiers.md#stream-identifiers-from-accessors), ActionCable Connections can designate that they are able to be `identified_by` one or more objects. These can be strings or ActiveRecord model resources. It is **only** using one of these connection identifiers that you can forcibly disconnect a client connection entirely. From 7d76a3e8a5770875fd51bea9f3035554bb4903df Mon Sep 17 00:00:00 2001 From: leastbad <38150464+leastbad@users.noreply.github.com> Date: Sun, 17 Jan 2021 09:02:08 -0500 Subject: [PATCH 07/10] multiple selector element operations (#92) * `select_all` option added to all operations which support a `selector` * `cancel` option added to almost all operations, intended to be set in a `before-` event callback * substantial reorganization of `cable_ready.js` to match the order in the docs --- javascript/cable_ready.js | 443 +++++++++++++++++++++----------------- javascript/utils.js | 11 + 2 files changed, 253 insertions(+), 201 deletions(-) diff --git a/javascript/cable_ready.js b/javascript/cable_ready.js index 1de82ed0..7bac44cd 100644 --- a/javascript/cable_ready.js +++ b/javascript/cable_ready.js @@ -1,6 +1,12 @@ import morphdom from 'morphdom' import { verifyNotMutable, verifyNotPermanent } from './callbacks' -import { assignFocus, dispatch, xpathToElement, getClassNames } from './utils' +import { + assignFocus, + dispatch, + xpathToElement, + getClassNames, + processElements +} from './utils' export let activeElement @@ -10,11 +16,11 @@ const didMorphCallbacks = [] // Indicates whether or not we should morph an element via onBeforeElUpdated callback // SEE: https://github.com/patrick-steele-idem/morphdom#morphdomfromnode-tonode-options--node // -const shouldMorph = detail => (fromEl, toEl) => { +const shouldMorph = operation => (fromEl, toEl) => { return !shouldMorphCallbacks .map(callback => { return typeof callback === 'function' - ? callback(detail, fromEl, toEl) + ? callback(operation, fromEl, toEl) : true }) .includes(false) @@ -22,251 +28,282 @@ const shouldMorph = detail => (fromEl, toEl) => { // Execute any pluggable functions that modify elements after morphing via onElUpdated callback // -const didMorph = detail => el => { +const didMorph = operation => el => { didMorphCallbacks.forEach(callback => { - if (typeof callback === 'function') callback(detail, el) + if (typeof callback === 'function') callback(operation, el) }) } -// Morphdom Callbacks ........................................................................................ - const DOMOperations = { - // Navigation .............................................................................................. - - pushState: config => { - const { state, title, url } = config - dispatch(document, 'cable-ready:before-push-state', config) - history.pushState(state || {}, title || '', url) - dispatch(document, 'cable-ready:after-push-state', config) + // DOM Mutations + + innerHtml: operation => { + processElements(operation, element => { + dispatch(element, 'cable-ready:before-inner-html', operation) + const { html, focusSelector } = operation + if (!operation.cancel) { + element.innerHTML = html + assignFocus(focusSelector) + } + dispatch(element, 'cable-ready:after-inner-html', operation) + }) }, - // Storage ................................................................................................. - - setStorageItem: config => { - const { key, value, type } = config - const storage = type === 'session' ? sessionStorage : localStorage - dispatch(document, 'cable-ready:before-set-storage-item', config) - storage.setItem(key, value) - dispatch(document, 'cable-ready:after-set-storage-item', config) + insertAdjacentHtml: operation => { + processElements(operation, element => { + dispatch(element, 'cable-ready:before-insert-adjacent-html', operation) + const { html, position, focusSelector } = operation + if (!operation.cancel) { + element.insertAdjacentHTML(position || 'beforeend', html) + assignFocus(focusSelector) + } + dispatch(element, 'cable-ready:after-insert-adjacent-html', operation) + }) }, - removeStorageItem: config => { - const { key, type } = config - const storage = type === 'session' ? sessionStorage : localStorage - dispatch(document, 'cable-ready:before-remove-storage-item', config) - storage.removeItem(key) - dispatch(document, 'cable-ready:after-remove-storage-item', config) + insertAdjacentText: operation => { + processElements(operation, element => { + dispatch(element, 'cable-ready:before-insert-adjacent-text', operation) + const { text, position, focusSelector } = operation + if (!operation.cancel) { + element.insertAdjacentText(position || 'beforeend', text) + assignFocus(focusSelector) + } + dispatch(element, 'cable-ready:after-insert-adjacent-text', operation) + }) }, - clearStorage: config => { - const { type } = config - const storage = type === 'session' ? sessionStorage : localStorage - dispatch(document, 'cable-ready:before-clear-storage', config) - storage.clear() - dispatch(document, 'cable-ready:after-clear-storage', config) + morph: operation => { + processElements(operation, element => { + const { html, childrenOnly, focusSelector } = operation + const template = document.createElement('template') + template.innerHTML = String(html).trim() + operation.content = template.content + dispatch(element, 'cable-ready:before-morph', operation) + const parent = element.parentElement + const ordinal = Array.from(parent.children).indexOf(element) + if (!operation.cancel) { + morphdom( + element, + childrenOnly ? template.content : template.innerHTML, + { + childrenOnly: !!childrenOnly, + onBeforeElUpdated: shouldMorph(operation), + onElUpdated: didMorph(operation) + } + ) + assignFocus(focusSelector) + } + dispatch(parent.children[ordinal], 'cable-ready:after-morph', operation) + }) }, - // Notifications ........................................................................................... - - consoleLog: config => { - const { message, level } = config - level && ['warn', 'info', 'error'].includes(level) - ? console[level](message) - : console.log(message) + outerHtml: operation => { + processElements(operation, element => { + dispatch(element, 'cable-ready:before-outer-html', operation) + const { html, focusSelector } = operation + const parent = element.parentElement + const ordinal = Array.from(parent.children).indexOf(element) + if (!operation.cancel) { + element.outerHTML = html + assignFocus(focusSelector) + } + dispatch( + parent.children[ordinal], + 'cable-ready:after-outer-html', + operation + ) + }) }, - notification: config => { - const { title, options } = config - dispatch(document, 'cable-ready:before-notification', config) - let permission - Notification.requestPermission().then(result => { - permission = result - if (result === 'granted') new Notification(title || '', options) - dispatch(document, 'cable-ready:after-notification', { - ...config, - permission - }) + remove: operation => { + processElements(operation, element => { + dispatch(element, 'cable-ready:before-remove', operation) + const { focusSelector } = operation + if (!operation.cancel) { + element.remove() + assignFocus(focusSelector) + } + dispatch(document, 'cable-ready:after-remove', operation) }) }, - // Cookies ................................................................................................. - - setCookie: config => { - const { cookie } = config - dispatch(document, 'cable-ready:before-set-cookie', config) - document.cookie = cookie - dispatch(document, 'cable-ready:after-set-cookie', config) + textContent: operation => { + processElements(operation, element => { + dispatch(element, 'cable-ready:before-text-content', operation) + const { text, focusSelector } = operation + if (!operation.cancel) { + element.textContent = text + assignFocus(focusSelector) + } + dispatch(element, 'cable-ready:after-text-content', operation) + }) }, - // DOM Events .............................................................................................. + // Element Property Mutations - dispatchEvent: config => { - const { element, name, detail } = config - dispatch(element, name, detail) + addCssClass: operation => { + processElements(operation, element => { + dispatch(element, 'cable-ready:before-add-css-class', operation) + const { name } = operation + if (!operation.cancel) element.classList.add(...getClassNames(name)) + dispatch(element, 'cable-ready:after-add-css-class', operation) + }) }, - // Element Mutations ....................................................................................... - - morph: detail => { - activeElement = document.activeElement - const { element, html, childrenOnly, focusSelector } = detail - const template = document.createElement('template') - template.innerHTML = String(html).trim() - dispatch(element, 'cable-ready:before-morph', { - ...detail, - content: template.content - }) - const parent = element.parentElement - const ordinal = Array.from(parent.children).indexOf(element) - morphdom(element, childrenOnly ? template.content : template.innerHTML, { - childrenOnly: !!childrenOnly, - onBeforeElUpdated: shouldMorph(detail), - onElUpdated: didMorph(detail) - }) - assignFocus(focusSelector) - dispatch(parent.children[ordinal], 'cable-ready:after-morph', { - ...detail, - content: template.content + removeAttribute: operation => { + processElements(operation, element => { + dispatch(element, 'cable-ready:before-remove-attribute', operation) + const { name } = operation + if (!operation.cancel) element.removeAttribute(name) + dispatch(element, 'cable-ready:after-remove-attribute', operation) }) }, - innerHtml: detail => { - activeElement = document.activeElement - const { element, html, focusSelector } = detail - dispatch(element, 'cable-ready:before-inner-html', detail) - element.innerHTML = html - assignFocus(focusSelector) - dispatch(element, 'cable-ready:after-inner-html', detail) + removeCssClass: operation => { + processElements(operation, element => { + dispatch(element, 'cable-ready:before-remove-css-class', operation) + const { name } = operation + if (!operation.cancel) element.classList.remove(...getClassNames(name)) + dispatch(element, 'cable-ready:after-remove-css-class', operation) + }) }, - outerHtml: detail => { - activeElement = document.activeElement - const { element, html, focusSelector } = detail - dispatch(element, 'cable-ready:before-outer-html', detail) - const parent = element.parentElement - const ordinal = Array.from(parent.children).indexOf(element) - element.outerHTML = html - assignFocus(focusSelector) - dispatch(parent.children[ordinal], 'cable-ready:after-outer-html', detail) + setAttribute: operation => { + processElements(operation, element => { + dispatch(element, 'cable-ready:before-set-attribute', operation) + const { name, value } = operation + if (!operation.cancel) element.setAttribute(name, value) + dispatch(element, 'cable-ready:after-set-attribute', operation) + }) }, - textContent: detail => { - activeElement = document.activeElement - const { element, text, focusSelector } = detail - dispatch(element, 'cable-ready:before-text-content', detail) - element.textContent = text - assignFocus(focusSelector) - dispatch(element, 'cable-ready:after-text-content', detail) + setDatasetProperty: operation => { + processElements(operation, element => { + dispatch(element, 'cable-ready:before-set-dataset-property', operation) + const { name, value } = operation + if (!operation.cancel) element.dataset[name] = value + dispatch(element, 'cable-ready:after-set-dataset-property', operation) + }) }, - insertAdjacentHtml: detail => { - activeElement = document.activeElement - const { element, html, position, focusSelector } = detail - dispatch(element, 'cable-ready:before-insert-adjacent-html', detail) - element.insertAdjacentHTML(position || 'beforeend', html) - assignFocus(focusSelector) - dispatch(element, 'cable-ready:after-insert-adjacent-html', detail) + setProperty: operation => { + processElements(operation, element => { + dispatch(element, 'cable-ready:before-set-property', operation) + const { name, value } = operation + if (!operation.cancel && name in element) element[name] = value + dispatch(element, 'cable-ready:after-set-property', operation) + }) }, - insertAdjacentText: detail => { - activeElement = document.activeElement - const { element, text, position, focusSelector } = detail - dispatch(element, 'cable-ready:before-insert-adjacent-text', detail) - element.insertAdjacentText(position || 'beforeend', text) - assignFocus(focusSelector) - dispatch(element, 'cable-ready:after-insert-adjacent-text', detail) + setStyle: operation => { + processElements(operation, element => { + dispatch(element, 'cable-ready:before-set-style', operation) + const { name, value } = operation + if (!operation.cancel) element.style[name] = value + dispatch(element, 'cable-ready:after-set-style', operation) + }) }, - remove: detail => { - activeElement = document.activeElement - const { element, focusSelector } = detail - dispatch(element, 'cable-ready:before-remove', detail) - element.remove() - assignFocus(focusSelector) - dispatch(element, 'cable-ready:after-remove', detail) + setStyles: operation => { + processElements(operation, element => { + dispatch(element, 'cable-ready:before-set-styles', operation) + const { styles } = operation + for (let [name, value] of Object.entries(styles)) { + if (!operation.cancel) element.style[name] = value + } + dispatch(element, 'cable-ready:after-set-styles', operation) + }) }, - setFocus: detail => { - activeElement = document.activeElement - const { element } = detail - dispatch(element, 'cable-ready:before-set-focus', detail) - assignFocus(element) - dispatch(element, 'cable-ready:after-set-focus', detail) + setValue: operation => { + processElements(operation, element => { + dispatch(element, 'cable-ready:before-set-value', operation) + const { value } = operation + if (!operation.cancel) element.value = value + dispatch(element, 'cable-ready:after-set-value', operation) + }) }, - setProperty: detail => { - const { element, name, value } = detail - dispatch(element, 'cable-ready:before-set-property', detail) - if (name in element) element[name] = value - dispatch(element, 'cable-ready:after-set-property', detail) - }, + // DOM Events - setValue: detail => { - activeElement = document.activeElement - const { element, value, focusSelector } = detail - dispatch(element, 'cable-ready:before-set-value', detail) - element.value = value - assignFocus(focusSelector) - dispatch(element, 'cable-ready:after-set-value', detail) + dispatchEvent: operation => { + processElements(operation, element => { + const { name, detail } = operation + dispatch(element, name, detail) + }) }, - // Attribute Mutations ..................................................................................... + // Browser Manipulations - setAttribute: detail => { - const { element, name, value } = detail - dispatch(element, 'cable-ready:before-set-attribute', detail) - element.setAttribute(name, value) - dispatch(element, 'cable-ready:after-set-attribute', detail) + clearStorage: operation => { + const { type } = operation + const storage = type === 'session' ? sessionStorage : localStorage + dispatch(document, 'cable-ready:before-clear-storage', operation) + if (!operation.cancel) storage.clear() + dispatch(document, 'cable-ready:after-clear-storage', operation) }, - removeAttribute: detail => { - const { element, name } = detail - dispatch(element, 'cable-ready:before-remove-attribute', detail) - element.removeAttribute(name) - dispatch(element, 'cable-ready:after-remove-attribute', detail) + pushState: operation => { + const { state, title, url } = operation + dispatch(document, 'cable-ready:before-push-state', operation) + if (!operation.cancel) history.pushState(state || {}, title || '', url) + dispatch(document, 'cable-ready:after-push-state', operation) }, - // CSS Class Mutations ..................................................................................... - - addCssClass: detail => { - const { element, name } = detail - dispatch(element, 'cable-ready:before-add-css-class', detail) - element.classList.add(...getClassNames(name)) - dispatch(element, 'cable-ready:after-add-css-class', detail) + removeStorageItem: operation => { + const { key, type } = operation + const storage = type === 'session' ? sessionStorage : localStorage + dispatch(document, 'cable-ready:before-remove-storage-item', operation) + if (!operation.cancel) storage.removeItem(key) + dispatch(document, 'cable-ready:after-remove-storage-item', operation) }, - removeCssClass: detail => { - const { element, name } = detail - dispatch(element, 'cable-ready:before-remove-css-class', detail) - element.classList.remove(...getClassNames(name)) - dispatch(element, 'cable-ready:after-remove-css-class', detail) + setCookie: operation => { + const { cookie } = operation + dispatch(document, 'cable-ready:before-set-cookie', operation) + if (!operation.cancel) document.cookie = cookie + dispatch(document, 'cable-ready:after-set-cookie', operation) }, - // Style Mutations ....................................................................................... - - setStyle: detail => { - const { element, name, value } = detail - dispatch(element, 'cable-ready:before-set-style', detail) - element.style[name] = value - dispatch(element, 'cable-ready:after-set-style', detail) + setFocus: operation => { + processElements(operation, element => { + dispatch(element, 'cable-ready:before-set-focus', operation) + if (!operation.cancel) assignFocus(element) + dispatch(element, 'cable-ready:after-set-focus', operation) + }) }, - setStyles: detail => { - const { element, styles } = detail - dispatch(element, 'cable-ready:before-set-styles', detail) - for (let [name, value] of Object.entries(styles)) { - element.style[name] = value - } - dispatch(element, 'cable-ready:after-set-styles', detail) + setStorageItem: operation => { + const { key, value, type } = operation + const storage = type === 'session' ? sessionStorage : localStorage + dispatch(document, 'cable-ready:before-set-storage-item', operation) + if (!operation.cancel) storage.setItem(key, value) + dispatch(document, 'cable-ready:after-set-storage-item', operation) }, - // Dataset Mutations ....................................................................................... + // Notifications + + consoleLog: operation => { + const { message, level } = operation + level && ['warn', 'info', 'error'].includes(level) + ? console[level](message) + : console.log(message) + }, - setDatasetProperty: detail => { - const { element, name, value } = detail - dispatch(element, 'cable-ready:before-set-dataset-property', detail) - element.dataset[name] = value - dispatch(element, 'cable-ready:after-set-dataset-property', detail) + notification: operation => { + const { title, options } = operation + dispatch(document, 'cable-ready:before-notification', operation) + let permission + if (!operation.cancel) + Notification.requestPermission().then(result => { + permission = result + if (result === 'granted') new Notification(title || '', options) + }) + dispatch(document, 'cable-ready:after-notification', { + ...operation, + permission + }) } } @@ -278,26 +315,30 @@ const perform = ( if (operations.hasOwnProperty(name)) { const entries = operations[name] for (let i = 0; i < entries.length; i++) { - const detail = entries[i] + const operation = entries[i] try { - if (detail.selector) { - detail.element = detail.xpath - ? xpathToElement(detail.selector) - : document.querySelector(detail.selector) + if (operation.selector) { + operation.element = operation.xpath + ? xpathToElement(operation.selector) + : document[ + operation.selectAll ? 'querySelectorAll' : 'querySelector' + ](operation.selector) } else { - detail.element = document + operation.element = document + } + if (operation.element || options.emitMissingElementWarnings) { + activeElement = document.activeElement + DOMOperations[name](operation) } - if (detail.element || options.emitMissingElementWarnings) - DOMOperations[name](detail) } catch (e) { - if (detail.element) { + if (operation.element) { console.error( `CableReady detected an error in ${name}! ${e.message}. If you need to support older browsers make sure you've included the corresponding polyfills. https://docs.stimulusreflex.com/setup#polyfills-for-ie11.` ) console.error(e) } else { console.log( - `CableReady ${name} failed due to missing DOM element for selector: '${detail.selector}'` + `CableReady ${name} failed due to missing DOM element for selector: '${operation.selector}'` ) } } diff --git a/javascript/utils.js b/javascript/utils.js index 550c908b..aa35f92b 100644 --- a/javascript/utils.js +++ b/javascript/utils.js @@ -50,3 +50,14 @@ export const xpathToElement = xpath => { // * names - could be a string or an array of strings for multiple classes. // export const getClassNames = names => Array(names).flat() + +// Perform operation for either the first or all of the elements returned by CSS selector +// +// * operation - the instruction payload from perform +// * callback - the operation function to run for each element +// +export const processElements = (operation, callback) => { + Array.from( + operation.selectAll ? operation.element : [operation.element] + ).forEach(callback) +} From 84e0aeafc50f9d52958b71ec777f58eab0e75c7e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 17 Jan 2021 14:02:43 +0000 Subject: [PATCH 08/10] Bump nokogiri from 1.10.10 to 1.11.1 Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.10.10 to 1.11.1. - [Release notes](https://github.com/sparklemotion/nokogiri/releases) - [Changelog](https://github.com/sparklemotion/nokogiri/blob/master/CHANGELOG.md) - [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.10.10...v1.11.1) Signed-off-by: dependabot[bot] --- Gemfile.lock | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1277a987..484e0028 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -102,13 +102,14 @@ GEM method_source (0.9.2) mimemagic (0.3.5) mini_mime (1.0.2) - mini_portile2 (2.4.0) + mini_portile2 (2.5.0) minitest (5.14.2) multi_json (1.15.0) multipart-post (2.1.1) nio4r (2.5.4) - nokogiri (1.10.10) - mini_portile2 (~> 2.4.0) + nokogiri (1.11.1) + mini_portile2 (~> 2.5.0) + racc (~> 1.4) octokit (4.19.0) faraday (>= 0.9) sawyer (~> 0.8.0, >= 0.5.3) @@ -121,6 +122,7 @@ GEM pry-nav (0.3.0) pry (>= 0.9.10, < 0.13.0) public_suffix (4.0.6) + racc (1.5.2) rack (2.2.3) rack-test (1.1.0) rack (>= 1.0, < 3) From 3b3fbeb5d4956a85666169f9e967b2736dcd62f0 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Sun, 17 Jan 2021 16:41:45 +0100 Subject: [PATCH 09/10] Add `append`, `prepend` and `replace` operations (#90) --- javascript/cable_ready.js | 38 +++++++++++++++++++++++++++++++++++++ lib/cable_ready/channels.rb | 3 +++ 2 files changed, 41 insertions(+) diff --git a/javascript/cable_ready.js b/javascript/cable_ready.js index 7bac44cd..93d08fc7 100644 --- a/javascript/cable_ready.js +++ b/javascript/cable_ready.js @@ -37,6 +37,18 @@ const didMorph = operation => el => { const DOMOperations = { // DOM Mutations + append: operation => { + processElements(operation, element => { + dispatch(element, 'cable-ready:before-append', operation) + const { html, focusSelector } = operation + if (!operation.cancel) { + element.insertAdjacentHTML('beforeend', html) + assignFocus(focusSelector) + } + dispatch(element, 'cable-ready:after-append', operation) + }) + }, + innerHtml: operation => { processElements(operation, element => { dispatch(element, 'cable-ready:before-inner-html', operation) @@ -116,6 +128,18 @@ const DOMOperations = { }) }, + prepend: operation => { + processElements(operation, element => { + dispatch(element, 'cable-ready:before-prepend', operation) + const { html, focusSelector } = operation + if (!operation.cancel) { + element.insertAdjacentHTML('afterbegin', html) + assignFocus(focusSelector) + } + dispatch(element, 'cable-ready:after-prepend', operation) + }) + }, + remove: operation => { processElements(operation, element => { dispatch(element, 'cable-ready:before-remove', operation) @@ -128,6 +152,20 @@ const DOMOperations = { }) }, + replace: operation => { + processElements(operation, element => { + dispatch(element, 'cable-ready:before-replace', operation) + const { html, focusSelector } = operation + const parent = element.parentElement + const ordinal = Array.from(parent.children).indexOf(element) + if (!operation.cancel) { + element.outerHTML = html + assignFocus(focusSelector) + } + dispatch(parent.children[ordinal], 'cable-ready:after-replace', operation) + }) + }, + textContent: operation => { processElements(operation, element => { dispatch(element, 'cable-ready:before-text-content', operation) diff --git a/lib/cable_ready/channels.rb b/lib/cable_ready/channels.rb index 804f05af..dba0a5e1 100644 --- a/lib/cable_ready/channels.rb +++ b/lib/cable_ready/channels.rb @@ -15,6 +15,7 @@ def initialize @channels = {} @operations = {} %i[ + append add_css_class clear_storage console_log @@ -25,11 +26,13 @@ def initialize morph notification outer_html + prepend push_state remove remove_attribute remove_css_class remove_storage_item + replace set_attribute set_cookie set_dataset_property From eebd4f5a9b47137f3abb5b471ecebb18e1afbb7a Mon Sep 17 00:00:00 2001 From: Hopsoft Date: Thu, 21 Jan 2021 05:56:12 -0700 Subject: [PATCH 10/10] Disable chaining after broadcast --- lib/cable_ready/channel.rb | 2 -- lib/cable_ready/config.rb | 7 ++++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/cable_ready/channel.rb b/lib/cable_ready/channel.rb index cec1650a..a69cc65a 100644 --- a/lib/cable_ready/channel.rb +++ b/lib/cable_ready/channel.rb @@ -17,13 +17,11 @@ def initialize(identifier) def broadcast(clear: true) ActionCable.server.broadcast identifier, {"cableReady" => true, "operations" => broadcastable_operations} reset if clear - self end def broadcast_to(model, clear: true) identifier.broadcast_to model, {"cableReady" => true, "operations" => broadcastable_operations} reset if clear - self end def add_operation_method(name) diff --git a/lib/cable_ready/config.rb b/lib/cable_ready/config.rb index 92d06f66..90b99f4b 100644 --- a/lib/cable_ready/config.rb +++ b/lib/cable_ready/config.rb @@ -7,11 +7,12 @@ module CableReady # This class is a process level singleton shared by all threads: CableReady::Config.instance class Config - include Singleton + include MonitorMixin include Observable + include Singleton def initialize - @lock = Monitor.new + super @operation_names = Set.new(default_operation_names) end @@ -20,7 +21,7 @@ def operation_names end def add_operation_name(name) - @lock.synchronize do + synchronize do @operation_names << name.to_sym notify_observers name.to_sym end