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

bulk_update() { || ... } click to toggle source

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
current_objects() click to toggle source

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
current_observers() click to toggle source

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
delay_updates?(object) click to toggle source

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
ignore_mutations() { || ... } click to toggle source
# 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
mutated!(object) click to toggle source

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() click to toggle source

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
observed!(object) click to toggle source

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
observed?(object) click to toggle source

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(exclusions) click to toggle source

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
observing(observer, immediate_update, rendering, update_objects) { || ... } click to toggle source

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
remove(observer = @current_observer) click to toggle source

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(observer) click to toggle source

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() click to toggle source

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
schedule_delayed_updater(object) click to toggle source

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
update_exclusions() click to toggle source

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
update_objects_to_observe(observer = @current_observer) click to toggle source

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