omniscientjs / immstruct

Immutable data structures with history for top-to-bottom properties in component based libraries like React. Based on Immutable.js
374 stars 21 forks source link

Listen for changes to specific paths #18

Closed torgeir closed 9 years ago

torgeir commented 9 years ago

We can easily add listeners to when specific pieces of data inside a structure changes.

Of course, you can aldready do

structure.on('change', (path, _, _) => {
  if (path === 'this.path') doThis();
  else if (path == 'something.else') doSomethingElse();
});

But something along the lines of this would be nice

structure.on('change', 'this.path', doThis);
structure.on('change', 'something.else', doSomethingElse);
dashed commented 9 years ago

I did some work regarding this on a library that would "decorate" immstruct, by providing utilities to make flux/isomorphic easier by focusing on immutable structures as the central store.

Some caveats I faced was that any element of the keyPath array can be of any type. So you can't use express-style routing as suggested in https://github.com/facebook/immutable-js/issues/237, nor can you use various amazing routing utilities like https://github.com/aaronblohowiak/routes.js, https://github.com/Matt-Esch/http-hash, or https://github.com/pillarjs/path-to-regexp.

I don't have much code to show yet, since it's still in heavy development. Here is how I would fetch or create node paths that reflect the keyPath:

var rootNode = new Structure();

function fetchNode(rootNode, keyPath, create) {
    var
    current = rootNode,
    i = 0;

    for(i = 0; i < keyPath.length; i++) {

        var children = current.get('children', NOT_SET);
        if(children === NOT_SET) {
            if(create) {
                children = current.set('children', Immutable.Map()).get('children');
            } else {
                current = null;
                break;
            }
        }

        var key = keyPath[i];
        var current = children.get(key, NOT_SET);
        if(current === NOT_SET) {
            if(create) {
                current = children.set(key, Immutable.Map()).get(key);
            } else {
                current = null;
                break;
            }
        }
    }

    return current;
}
dashed commented 9 years ago

Since we can't do something like matching '/foo/:bar' paths as in express. I'm mulling over the idea of listening via the bubbling and capture event model as is done in DOM.

torgeir commented 9 years ago

immstruct already knows the path of .update()s, can't we simply (in pseudo code)

function on (ev, key, fn) {
  structure.on(ev, function (path, old, neww) {
    if (path === key)
      fn.apply(null, arguments);
  });
}
dashed commented 9 years ago

I was thinking something similar, but I decided to go with a structure similar to a trie tree to be efficient on space complexity in the long term.

dashed commented 9 years ago

I've been experimenting with implementing immstruct.listenTo, and I've run into some interesting issues:

// some promise returning function
var a_listener = co.wrap(function*(results){

    // Input:
    // results = {
    //     event: 'change',
    //     newValue: ...,
    //     oldValue: ...
    // };

    // update to execute another listener
    struct.cursor('b').update(function(m) {
        return m.set('foo', 'bar');
    });

    // suggestion: wait until listener for ['b'] finishes?
    yield immstruct.waitFor(struct, 'b');

    // update keypath with no listener
    struct.cursor('c').update(function(m) {
        return m.set('foo', 'bar');
    });

});

var b_listener = co.wrap(function*(results){

    // how to handle cycles?
    struct.cursor('a').update(function(m) {
        return m.set('foo', 'bar');
    });

});

var struct = immstruct({ a: {}, b: {}, c: {} });

immstruct.listenTo(struct, ['a'], a_listener);
immstruct.listenTo(struct, ['b'], b_listener);

struct.cursor('a').update(function(m) {
    return m.set('foo', 0);
});

// how to handle case when modifying cursor while listener for ['a'] executes?
struct.cursor('a').update(function(m) {
    return m.set('foo', 1);
});
mikaelbr commented 9 years ago

Reference cursors should cover this part, no? #30

dashed commented 9 years ago

Yep. This should be closed.