Skip to content
Daniel Ly edited this page Aug 30, 2017 · 2 revisions

Deep object tree observation with cycle prevention

observe-tree is a simple and little JavaScript library. It offers deep object tree observation. Cycles are detected and cause a TypeError. Changing something inside an observed tree cause callbacks with change records.

What is the use case of the library?

I am developing a simple web component to implement the Flux pattern. For this I need a state store on which I can detect changes. I am sure that there are other use cases. Whenever you need to know when and what has changed in a deep tree, perhaps this library is useful to you.

How does the library detect changes?

An ECMAScript 2015 feature is used: the Proxy. The objects inside the tree are wrapped in a Proxy, and the set trap causes a callback.

Which types can you store in the tree?

You can store primitives (except undefined), plain objects, arrays and Date. Other types cause a TypeError. Usually you should convert them first. For example by Object.assign({}, nonPlainObject). This limitation is important to keep the implementation simple.

How does cycle prevention work?

observe-tree uses a Map to remember objects in the tree. If an object is already in the map and you add it to the tree, you create a cycle. For example

const tree = observeTree({}, change => console.log(change)
tree.tree = tree

will cause a TypeError. This is important to prevent infinite loops when walking the tree. This keeps the library simple.

What is passed in callbacks?

The change record in the callback is a plain object with the fields path, oldValue and newValue. path is an array of string containing the field names for descending into the tree. oldValue and newValue are the values inside the tree affected by the change. If a new field is created inside an object, oldValue is omitted. If a field is deleted, newValue is omitted. If the value is an object, an array or a date, pass a reference, not a clone.

API

This is inspired by TypeScript and Flow.

type Primitive = string | number | boolean | symbol | null
type PlainObject = Array | Date | { [string]: PlainObject | Primitive }
type ChangeRecord = { 
  path: [ string ],                   # access path inside the tree
  ?newValue: PlainObject,             # omitted for creations
  ?oldValue: PlainObject,             # omitted for deletions
}
type observeTree = (
  tree: PlainObject,                  # object tree to be observed
  callback: ChangeRecord => void,     # call back on changes in the tree
) => Proxy<PlainObject>               # return the observed object

Example

const observable = observeTree({ a: 'alpha' }, change => console.log(change))
observable.b = 'beta'        // { path: [ 'b' ], newValue: 'beta' }
observable.a = [ 'alpha' ]   // { path: [ 'a' ], oldValue: 'alpha', newValue: [ 'alpha' ] }
observable.a[1] = 'one'      // { path: [ 'a', 1 ], newValue: 'one' }
delete observable.b          // { path: [ 'b' ], oldValue: 'beta' }
observable.a.splice(1, 1, 'x', 'y')
// { path: [ 'a', 1 ], oldValue: 'one', newValue: 'x' }
// { path: [ 'a', 2 ], newValue: 'y' }
observable.c = observable    // TypeError('observeTree() not supported: cycle at c')
observable.c = function() {} // TypeError('observeTree() not supported: object of class Function')

Roadmap

This software is not yet ready for production use.

Things to be done first:

  1. Configure continuous integration
  2. Configure code coverage
  3. Implement all tests
  4. Use the library in a different project

However some tests are implemented and they pass. Perhaps this library is already useful.

Long-term objectives

  • Keep the library small and simple. Prefer to throw TypeError on edge cases rather than solve them.
  • Use modern JavaScript technologies like linters or advanced built-in objects like Proxy or Map.
  • However don't use transpilers (except maybe for tests).
  • The library's baseline is ECMAScript 2015.
  • Formatting
    • omit optional semicolons (let the linter warn about traps like lines beginning with [)
    • limit line length to 80

Open questions

  • Are symbol keys supported?
  • Should we call back with an error instead of throwing TypeError?
Clone this wiki locally