class ObservableCollection

This class facilitates the modification of collections in a distributed fashion. It adds the Observable functionality to collections types like Hash and Array. Initialize with the underlying collection you want to wrap.

It supports nested collections. Notifications of changes in lower levels in a data structure are bubbled upward by chained-together ObservableCollections, where each chain link is an observable-observer relationship.

The object wrapped by an ObservableCollection can be accessed explicitly via subject, but the point of this class is that you can treat an ObservableCollection as you would a regular Array or Hash.

Constants

DESTRUCTIVE

The use of one of these methods may result in the subject changing

Attributes

subject[RW]

Public Class Methods

create(subject, observer = nil, opts = {}) click to toggle source

Factory method - takes in an Array or a Hash, and the observer. Options are passed to constructor (see above). An additional option is :func, which specifies the name of the observer’s update callback (defaults to :update as in the Observable module).

# File lib/observable_collection.rb, line 40
def self.create(subject, observer = nil, opts = {})
  observable = subject
  if [Array, Hash].include? observable.class
    observable = ObservableCollection.new(subject, opts)
    args = [observer]
    args << opts[:func] if opts[:func]
    observable.add_observer(*args) if observer
  end
  observable
end
new(subject, opts = {}) click to toggle source

Options:

lock_file - path to a file used for locking
always_update_after - always call update after the collection is accessed,
                      regardless of whether the methods are destructive
                      (update is always called *before* the collection
                      is accessed)
# File lib/observable_collection.rb, line 30
def initialize(subject, opts = {})
  @subject = subject
  @lock_file = opts[:lock_file]
  @always_update_after = opts[:always_update_after]
end

Public Instance Methods

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

Gain an exclusive lock on access to this data structure. Accepts a block to execute while the lock is owned. It is best to do this whenever, e.g., writing to disk upon changes to the collection.

# File lib/observable_collection.rb, line 54
def lock
  _lock

  # (TL;DR: locking solves more problems than concurrency)
  # Only read from disk once while the lock is kept. Normal behavior is
  # to read every time a method is called at any level of the data
  # structure, which can cause problems when e.g. reading twice in one
  # line, such as `obs_hash[a][b] << obs_hash[c][d].count`. Note that the
  # problem being solved here is not related to concurrency--it's just
  # a convenient way to solve it.
  changed
  notify_observers(self, :before)

  yield

  _unlock
end
method_missing(meth, *args, &block) click to toggle source

Users will treat ObservableCollection like a regular collection, so send method calls to the underlying collection. Extra things we do:

-catch the creation/retrieval of child collections and make them
 observable too, so that updates to them bubble up.
-let *our* observers know about this method call, both before and after
 we call the desired method.
# File lib/observable_collection.rb, line 79
def method_missing(meth, *args, &block)

  # Let the our observers know someone is calling a method on us. If we are
  # reporting directly to a user-land observer, its callback will be
  # invoked. If we are reporting to another ObservableCollection, it will
  # just propagate the notification upward.
  unless @locked
    changed
    notify_observers(@subject, :before)
  end

  # Execute the method on the subject
  result = @subject.send(meth, *args, &block)

  # If the return value is another ObservableCollection, add myself as an
  # observer. If it's an ordinary collection, make it an ObservableCollection
  # and add myself as an observer. The exception is when result == @subject,
  # in which case we just want to return the subject unadorned. This is to
  # avoid, e.g., puts being unable to convert an observable array to a regular
  # array the way it expects (this exception facilitates, e.g., `puts hash.values`)
  if result.is_a? ObservableCollection
    result.add_observer self
  elsif result != @subject
    result = ObservableCollection.create(result, self,
                                         lock_file: @lock_file,
                                         always_update_after: @always_update_after)
  end

  if (DESTRUCTIVE.include? meth) || @always_update_after
    changed
    notify_observers(@subject, :after)
  end

  result
end
update(_downstream_object, kind) click to toggle source

We ignore the arguments because we don’t care what the change was downstream–we just need to propagate upward the message that something changed.

# File lib/observable_collection.rb, line 118
def update(_downstream_object, kind)
  if kind == :after
    changed
    notify_observers(@subject, :after)
  end
end

Protected Instance Methods

_lock() click to toggle source
# File lib/observable_collection.rb, line 127
def _lock
  unless @lock_file
    throw "Can't use lock feature without specifying a lock file"
  end
  @lock = File.open(@lock_file, 'a+')
  @lock.flock(File::LOCK_EX)
  @locked = true
end
_unlock() click to toggle source
# File lib/observable_collection.rb, line 136
def _unlock
  @lock.close
  @locked = false
end