Class: ObservableCollection

Inherits:
Object
  • Object
show all
Includes:
Observable
Defined in:
lib/observable_collection.rb

Overview

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.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(subject, opts = {}) ⇒ ObservableCollection

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)


30
31
32
33
34
# 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

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(meth, *args, &block) ⇒ Object

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.


79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# 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

Instance Attribute Details

#subjectObject

Returns the value of attribute subject.



22
23
24
# File 'lib/observable_collection.rb', line 22

def subject
  @subject
end

Class Method Details

.create(subject, observer = nil, opts = {}) ⇒ Object

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).



40
41
42
43
44
45
46
47
48
49
# 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

Instance Method Details

#lockObject

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.



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# 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

#update(_downstream_object, kind) ⇒ Object

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.



118
119
120
121
122
123
# File 'lib/observable_collection.rb', line 118

def update(_downstream_object, kind)
  if kind == :after
    changed
    notify_observers(@subject, :after)
  end
end