module Hyperstack::Internal::State::Mapper
Typically mutated! is called during some javascript event, and we will want to delay notification until the event handler has completed execution.
Public Class Methods
Code can be wrapped in the bulk_update
method, and notifications of any mutations that occur during the yield will be scheduled for after the current event finishes.
# File lib/hyperstack/internal/state/mapper.rb, line 97 def bulk_update saved_bulk_update_flag = @bulk_update_flag @bulk_update_flag = true yield ensure @bulk_update_flag = saved_bulk_update_flag end
and a list of objects indexed by observers
# File lib/hyperstack/internal/state/mapper.rb, line 166 def current_objects @current_objects ||= Hash.new { |h, k| h[k] = [] } end
at the end of the rendering cycle the new_objects
are processed into a list of observers indexed by objects…
# File lib/hyperstack/internal/state/mapper.rb, line 161 def current_observers @current_observers ||= Hash.new { |h, k| h[k] = [] } end
determine if updates should be delayed. always delay updates if the bulk_update_flag is set otherwise delayed updates only occurs if Hyperstack.on_client? is true WITH ONE EXCEPTION: observers can indicate that they need immediate updates in case that the object being updated is themselves.
# File lib/hyperstack/internal/state/mapper.rb, line 209 def delay_updates?(object) @bulk_update_flag || (Hyperstack.on_client? && (@immediate_update != @current_observer || @current_observer != object)) end
# File lib/hyperstack/internal/state/mapper.rb, line 105 def ignore_mutations saved_ignore_mutations_flag = @ignore_mutations @ignore_mutations = true yield ensure @ignore_mutations = saved_ignore_mutations_flag end
Called when an object has been mutated. Depending on the state of StateContext we will either schedule the update notification for later, immediately notify any observers, or do nothing.
# File lib/hyperstack/internal/state/mapper.rb, line 75 def mutated!(object) return if @ignore_mutations if delay_updates?(object) schedule_delayed_updater(object) elsif @rendering_level.zero? current_observers[object].each do |observer| observer.mutations([object]) end if current_observers.key? object end end
new_objects
are added as the @current_observer reads an objects state
# File lib/hyperstack/internal/state/mapper.rb, line 155 def new_objects @new_objects ||= Hash.new { |h, k| h[k] = Set.new } end
called when an object has been observed (i.e. read) by somebody
# File lib/hyperstack/internal/state/mapper.rb, line 64 def observed!(object) return unless @current_observer new_objects[@current_observer] << object return unless update_exclusions[object] update_exclusions[object] << @current_observer end
Check to see if an object has been observed.
# File lib/hyperstack/internal/state/mapper.rb, line 87 def observed?(object) # we don't want to unnecessarily create a reference to ourselves # in the current_observers hash so we just look for the key. current_observers.key?(object)# && current_observers[object].any? end
observers_to_update
returns a hash with observers as keys, and lists of objects as values. The hash is built by filtering the current_observers
list including only observers that have mutated objects, that are not on the exclusion list.
# File lib/hyperstack/internal/state/mapper.rb, line 260 def observers_to_update(exclusions) Hash.new { |hash, key| hash[key] = Array.new }.tap do |updates| exclusions.each do |object, excluded_observers| current_observers[object].each do |observer| next if excluded_observers.include?(observer) updates[observer] << object end if current_observers.key? object end end end
Once the observer's block completes execution, the context instance variables are restored.
# File lib/hyperstack/internal/state/mapper.rb, line 45 def observing(observer, immediate_update, rendering, update_objects) saved_context = [@current_observer, @immediate_update] @current_observer = observer @immediate_update = immediate_update && observer if rendering @rendering_level += 1 observed!(observer) observed!(observer.class) end return_value = yield update_objects_to_observe(observer) if update_objects return_value ensure @current_observer, @immediate_update = saved_context @rendering_level -= 1 if rendering return_value end
call remove before unmounting components to prevent stray events from being sent to unmounted components.
# File lib/hyperstack/internal/state/mapper.rb, line 141 def remove(observer = @current_observer) remove_current_observers_and_objects(observer) new_objects.delete observer # see run_delayed_updater for the purpose of @removed_observers @removed_observers << observer if @removed_observers end
remove_current_observers_and_objects
clears the hashes between renders
# File lib/hyperstack/internal/state/mapper.rb, line 189 def remove_current_observers_and_objects(observer) raise 'state management called outside of watch block' unless observer deleted_objects = current_objects.delete(observer) return unless deleted_objects deleted_objects.each do |object| # to allow for GC we never want objects hanging around as keys in # the current_observers hash, so we tread carefully here. next unless current_observers.key? object current_observers[object].delete(observer) current_observers.delete object if current_observers[object].empty? end end
run_delayed_updater
will call the mutations method for each observer passing the entire list of objects that changed while waiting for the delay except those that the observer has already seen (the exclusion list). The observers mutation method may cause some other observer already on the observers_to_update
list to be removed. To prevent these observers from receiving mutations we keep a temporary set of removed_observers. This is initialized before the mutations, and then cleared as soon as we are done.
# File lib/hyperstack/internal/state/mapper.rb, line 244 def run_delayed_updater current_update_exclusions = @update_exclusions @update_exclusions = @delayed_updater = nil @removed_observers = Set.new observers_to_update(current_update_exclusions).each do |observer, objects| observer.mutations objects unless @removed_observers.include? observer end ensure @removed_observers = nil end
If an object changes state again then the Set will be reinitialized, and all the observers that might have been on a previous exclusion list, will now be notified.
# File lib/hyperstack/internal/state/mapper.rb, line 231 def schedule_delayed_updater(object) update_exclusions[object] = Set.new @delayed_updater ||= after(0) { run_delayed_updater } end
We avoid keeping empty lists of observers on the exclusion lists by not adding an object hash key unless the object already has pending state changes. (See the schedule_delayed_updater
method below)
# File lib/hyperstack/internal/state/mapper.rb, line 183 def update_exclusions @update_exclusions ||= Hash.new end
TODO: see if we can get rid of all this and simply calling remove_current_observers_and_objects
at the START of each components rendering cycle (i.e. before_mount and before_update)
# File lib/hyperstack/internal/state/mapper.rb, line 132 def update_objects_to_observe(observer = @current_observer) remove_current_observers_and_objects(observer) objects = new_objects.delete(observer) objects.each { |object| current_observers[object] << observer } if objects current_objects[observer] = objects end