Observable Objects: Model

Goal

The first primitive in the MDV design is a Model abstraction which creates a mechanism through which mutations to objects can be expressed directly, but allows observers to be notified when mutations occur.

The Model abstraction allows for two types of observation:

  1. Observe all mutations which occur to a specific object.
  2. Observe changes to the value at a path from an object.

Further, it guarantees that

  1. Observers are notified if and only if a change has occurred.
  2. Observers are notified in a particular order.
<script>
var data = {person: { name: 'Jim', age: 32 }};
var m = Model.get(data);

Model.observeObject(m, function(change) {
  // Handle property add/update/deletes
});

Model.observe(m, "person.age", function(value, oldValue) {
  // Handle value change of m.person.age
});

m.person.age = 33; // Second observer fires: (value = 33, oldValue=32)
m.parent = { name: 'Sam', age: 72 }; // First observer fires (property 'parent' added)
data.person.name = 'James'; // No observers are fired. Only model objects are observable.
</script>

Path

Note: the code samples in this section are pseudo-code.

A Path is generally a JSON-style path. It is modelled after the member lookup syntax in ECMAScript. Dot (.) and bracket ([]) notation are supported.

{"a": {"b": {"c": 42, "d": [97, 13]}}}

a.b.c => 42
a.b.d[1] => 13

A path can also contain parent (../), this (./) and root (/). The syntax for these where inspired by the file system path syntax. It is important to point out that even though a path can reach above itself, whenever it is used it will never be able to reach above the reference object.

Parent

The parent path name (../) allows you to select a value above the current reference point. In general this is not that useful since you cannot reach above the current reference object. However, having a parent selector is useful when concatenating paths. When concatenating multiple paths the parent strips the previous part.

path-a = a.b.c
path-b = ../d
concat(path-a, path-b) => a.b.d

This

The this path name (./) allows you to select the value at the current reference point.

path-a = a.b.c
path-b = ./
concat(path-a, path-b) => a.b.c

Root

The root path name (/) selects the root of the reference. It's value is that, when concatenating multiple path it strips everything to the left of the root part.

path-a = a.b.c
path-b = /d.e
concat(path-a, path-b) => /d.e

Path Value

A Path Value is the value of a property at a path from a given reference object. If the path is unreachable, the value is considered undefined.

Needs to be formalized.

Model Objects

A Model object is a proxy for an underlying "raw" data object. The model can be read and written to as if it were the underlying object, but writes will cause observers to be notified of mutations. The object which the model is proxing is the proxied object. An object has at most one model and a model has at most one proxied object. Models have the following behaviors.

Further:

Clarify. The point here is that writes to models will fire notifications to observers. Further that the model maintain a wet/dry boundary for the models/proxied objects. The wet side is the "outside", i.e. traversing from a model will always return objects wrapped as models. The dry side is the "inside", i.e. functions of proxied objects (including setters) should never see model objects, only proxied "raw" objects.

Arrays objects behave like other objects with respect to models, except that Array fire "splice" mutation events when index properties are affected.

Model.get

Model.get(object, [opt_path])

Create or fetch the existing model for a the value at a opt_path from object.

The Model maintains an internal weak mapping between raw objects and model objects. At most, one model object will exist for a given raw object.

The call behaves as follows:

Model.observeObject

Model.observeObject(object, callback)

Register a callback to be invoked every time object is mutated.

The call behaves as follows:

Model.observe

Model.observe(object, path, callback)

Register a callback to be invoked when the value at path from the object changes.

The call behaves as follows:

Observer notification sequencing

Observers affected by an object mutation are notified in an order which reflects decreasing proximity to the mutation. Specifically:

  1. All Model.observeObject observers of the object that was mutated
  2. All affected Model.observe observers, breadth-first, starting at one property away from the object and expanded until all are reached.

Open Issues