Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.reference() for neverending cursors #30

Merged
merged 1 commit into from
Feb 8, 2015
Merged

.reference() for neverending cursors #30

merged 1 commit into from
Feb 8, 2015

Conversation

torgeir
Copy link
Member

@torgeir torgeir commented Feb 5, 2015

A PR is easier for cooperation

@torgeir
Copy link
Member Author

torgeir commented Feb 5, 2015

This will allow for something like

var clock = quiz.reference('clock');

let Clock = component('Clock', [ forceUpdateOn(clock) ], () =>
  div({},
    String(clock.cursor().deref())));

function forceUpdateOn (ref) {
  var bound;
  return {
    componentDidMount: function () {
      bound = ref.onChange(_ => this.forceUpdate());
    },
    componentWillUnmount: function () {
      bound.remove();
    }
  };
}

as per omniscientjs/omniscient#42

@mikaelbr
Copy link
Member

mikaelbr commented Feb 5, 2015

The current implementation is more like:

var clock = quiz.reference('clock');

let Clock = component('Clock', [ forceUpdateOn(clock) ], () =>
  div({},
    String(clock.cursor().deref())));

function forceUpdateOn (ref) {
  return {
    componentDidMount: function () {
      ref.onChange(_ => this.forceUpdate());
    },
    componentWillUnmount: function () {
      ref.unsubscribe();
    }
  };
}

(As it is unsubscribe on a reference level, not onChange-level)

@mikaelbr
Copy link
Member

mikaelbr commented Feb 5, 2015

My main issue with this at this point is that it seems unnecessary to have many parallel equal listeners on "swap", instead of having one listener. One listener would make cleanup a more manual job, though.

I haven't tested the code extensively, nor have I written tests yet. This is just a concept / PoC, which we can discuss and possibly hack on. But initial run-through seems positive!


if (!this.current) {
throw new Error('No structure loaded.');
}

var changeListener = function (newRoot, oldRoot, path) {

onChange(newRoot, oldRoot, path);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should go onto its own changeListener function. i.e. changeListener = handleOnChange(this, changeListener);

Also, onChange(newRoot, oldRoot, path); shouldn't be called before self.current is set.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, agreed. This is just from early stages of experimentation and should probably not be in this PR, or maybe even be a part of the code at all. Should strive to only have relevant code and features we need.

@dashed
Copy link
Contributor

dashed commented Feb 5, 2015

Agree with @mikaelbr that registering the structure once with a centralized function is sufficient. I do something similar with my own immstruct wrapper: https://gist.github.com/Dashed/e38d4184bc239268737e#file-listen-js-L84-L120

this.on('swap', changeListener);

return {
onChange: function (newFn) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will need a way to "unsubscribe" functions. Otherwise, repeated calls to componentDidMount will attach functions, which will be repeated executed, causing an unnecessary amounts of cascading of function calls.

I'm basing this on this example:

var clock = quiz.reference('clock');

let Clock = component('Clock', [ forceUpdateOn(clock) ], () =>
  div({},
    String(clock.cursor().deref())));

function forceUpdateOn (ref) {
  return {
    componentDidMount: function () {
      ref.onChange(_ => this.forceUpdate());
    },
    componentWillUnmount: function () {
      ref.unsubscribe();
    }
  };
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify, fn reference still exists when calling onChange after calling unsubscribe.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this should probably be handled a bit different. Most likely, there's no need to pass in a function on reference creation when you have onChange as well.

@torgeir
Copy link
Member Author

torgeir commented Feb 6, 2015

Afaik, unsubscribing one listener and stopping a ref from any longer emitting changes are two separate things. One component should be able to unsubscribe its ref listeners without stopping other component's listeners

@mikaelbr
Copy link
Member

mikaelbr commented Feb 6, 2015

@torgeir For now it is on reference level, not affecting anyone other then themself, but having one listener in common for every reference, would require some more logic for unsubscribing on reference level, yeah.

@torgeir
Copy link
Member Author

torgeir commented Feb 6, 2015

That's my point excactly, you should be able to cancel subscritions as well as tear a whole ref

Edit: could probably be solved by using a list of functions that are looped and called if the key is the same on swap. unsubscribe() could remove the function instance from the list, while cancel could empty the list and remove the swap listener.

@dashed
Copy link
Contributor

dashed commented Feb 6, 2015

Why can't we make the function subscription a little bit smarter? We track by reference counting.

We subscribe changeListener to swap only when function passed to onChange is the first subscription. We unsubscribe changeListener from swap only when function removed from viaunsubscribe is the last listener.

changeListener subscription should be automatic. We, the users, shouldn't be handling this manually.

All we want and care about is adding and removing functions via onChange and unsubscribe.

return original.apply(this, arguments);
}
},
cursor: function () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're going to have this, a dual structure() should be provided to get the structure reference.

@dashed
Copy link
Contributor

dashed commented Feb 7, 2015

I think Structure.prototype.reference should be renamed to something else. It may be misleading in that it refers to a static internal object associated with keyPath.

Imagine having var clock = quiz.reference('clock'); in two different components. Calling clock.unsubscribeAll() in one component doesn't affect clock object in the other component.

Maybe Structure.prototype.scope?

@dashed
Copy link
Contributor

dashed commented Feb 7, 2015

@mikaelbr looking good so far 👍

@dashed
Copy link
Contributor

dashed commented Feb 7, 2015

Since Immutable is available. I'm wondering if the collections can be used instead of native object/array.

@mikaelbr
Copy link
Member

mikaelbr commented Feb 7, 2015

Since Immutable is available. I'm wondering if the collections can be used instead of native object/array.

I started out with immutable structure, but it didn't really make the code easier or improve it much. Don't see any real gain at this point.

@mikaelbr
Copy link
Member

mikaelbr commented Feb 7, 2015

Maybe Structure.prototype.scope?

We had a discussion on this, but we found that .reference still is more descriptive. You can have multiple references to a everlasting cursor, and I actually think this makes sense:

Imagine having var clock = quiz.reference('clock'); in two different components. Calling clock.unsubscribeAll() in one component doesn't affect clock object in the other component.

So, you are unsubscribing on all listeners for that reference, it shouldn't remove listeners on other references. They aren't the same references, just all references to the same data.

@dashed
Copy link
Contributor

dashed commented Feb 7, 2015

So, you are unsubscribing on all listeners for that reference, it shouldn't remove listeners on other references. They aren't the same references, just all references to the same data.

I need to think on this some more; maybe it'll grow on me 😛

/cc @skevy Thoughts?

var fnIndex, listenerNs = self._pathListeners[pathId];

self._pathListeners[pathId] = !listenerNs ? [newFn] : listenerNs.concat(newFn);
fnIndex = self._pathListeners[pathId].indexOf(newFn);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be better to instead use self._pathListeners[pathId].length - 1.

@dashed
Copy link
Contributor

dashed commented Feb 7, 2015

onChange is pretty low-level. I feel that there should be sugar functions onAdd, onDelete, andonUpdate that wrap upon onChange.

// abstracted out
let analyze = function(newData, oldData, path) {

    var oldObject = oldData && oldData.getIn(path);
    var newObject = newData && newData.getIn(path);

    var inOld = oldData && hasIn(oldData, path);
    var inNew = newData && hasIn(newData, path);

    return {
        oldObject,
        newObject,
        inOld,
        inNew
    };
}

let onAdd = function(newFn) {

    return this.onChange(function(newData, oldData, path) {

        let ret = analyze(newData, oldData, path)
        if(!ret.inOld && ret.inNew) {
            newFn(path, ret.newObject)
        }
    })
}

// similar to onDelete and onUpdate

@dashed
Copy link
Contributor

dashed commented Feb 7, 2015

This may require to rename change event to update to be semantically consistent.

unsubscribeAll: function () {
cursor = void 0;
if (!self._pathListeners[pathId]) {
referenceListeners = [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be referenceListeners = [changeListener];

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dashed
Copy link
Contributor

dashed commented Feb 7, 2015

We'll need a way to completely destroy "everlasting cursors". It'll probably require a dead man's swtich variable, to make all methods of the reference cursor to be in noop mode.

We need this, because self._pathListeners[pathId] will become polluted with changeListener functions, even if the reference cursor is GC'd.

referenceListeners = void 0;
cursor = void 0;

this.onChange = void 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. This is alright; but not solid. If, for some reason, someone assign a method to some variable, then the method can still be called. It might be solid enough to have if (dead) return; at the top of each method.

@dashed
Copy link
Contributor

dashed commented Feb 7, 2015

Throwing this idea in.

Since there is a cursor() method, I'm wondering if it's equally useful to have structure() method to get the structure object that's associated with the cursor/reference?

@@ -68,6 +77,54 @@ Structure.prototype.cursor = function (path) {
return Cursor.from(self.current, path, changeListener);
};

Structure.prototype.reference = function (path) {
var self = this, pathId = pathString(path);
var listenerNs = self._pathListeners[pathId];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering there should be another dimension to self._pathListeners[pathId]. One for reference cursors.

Example:
self._pathListeners[pathId][referenceKey][funIdx]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the current solution is a better approach in terms of iterating over and executing listeners on swap.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm.. I guess it's fine when self._pathListeners[pathId] isn't hugely packed.

I just naturally think of big O cases 😄

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the list will be too long. Maybe we need to see the use cases and revise this, but as for now, I think this works just as good.

@dashed
Copy link
Contributor

dashed commented Feb 7, 2015

Putting here for posterity: Suggesting to rename to Structure.observe(keyPath) which returns an "observer" object.

See: https://gitter.im/omniscientjs/omniscient?at=54d621c1c4386fab2709bf4c

self._pathListeners[pathId].splice(fnIndex, 1);
};
},
cursor: function () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we want to get sub-cursor via an everlasting cursor?

cursor: function (keyPath) {
  return cursor(keyPath);
},

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started something similar yesterday, but it wasnt that straight forward. You need to alter listener location (reference listener)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow. I'm thinking that these are equivalent

  • everlasting.cursor(keyPath)
  • everlasting.cursor().cursor(keyPath)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the patch I just committed, this will work better: 7333413

Problem, as you see, is that the path lookup didn't allow for sub-cursors.

@skevy
Copy link

skevy commented Feb 7, 2015

This is awesome that this is being worked on!

The only thing I would say with this is that the API for onChange is odd, simply because the structure is an EventEmitter, with .on. So I'm wondering if we also make the reference an EventEmitter, so that it the API between a structure and a reference is similar?

@dashed
Copy link
Contributor

dashed commented Feb 7, 2015

I'm throwing around this idea: https://gitter.im/omniscientjs/omniscient?at=54d6273cdb625d410e7c26bf

Usage story:

  • Structure.reference(keyPath) would return an "everlasting cursor".
  • And on this everlasting cursor, we can observer/unobserve on it.
  • And destroy it.
  • onChange and unsubscribeAll is renamed to observe and unobserveAll respectively.
  • .observe(event, listener) and .observer(listener) where event is either add, delete, change, or swap.
  • .observe(listener) is shorthand for .observe('swap', listener)

@skevy
Copy link

skevy commented Feb 7, 2015

@dashed you don't think using an EventEmitter and .on instead of .observe would be better? I was just thinking it'd be good to keep all the api's the same between the structure and the reference.

@dashed
Copy link
Contributor

dashed commented Feb 7, 2015

@skevy An EventEmitter is already used indirectly here:

this.on('swap', function (newData, oldData, keyPath) {
(self._pathListeners[pathString(keyPath)] || []).forEach(function (fn) {
if (typeof fn !== 'function') return;
fn(newData, oldData, keyPath);
});
});

To elaborate, Structure.reference(keyPath) is a more abstract concept wherein it creates and returns an everlasting cursor. An everlasting cursor holds a cursor of the same keyPath internally, and keeps that cursor fresh every time value at keyPath changes. Traditionally, cursors would become stale.

We add features to an everlasting cursor; such as observation. Through the everlasting cursor, we can 'observe' the internal cursor. That's why .on semantics (EventEmitter) isn't really used.

@dashed
Copy link
Contributor

dashed commented Feb 7, 2015

I was just thinking it'd be good to keep all the api's the same between the structure and the reference.

This may be a good argument against .observe(event, listener).

At the moment, onChange is way too low-level, in that it's pretty much equivalent to:

structure.on('swap', (newData, oldData ,path) => {
  if(!equalPath(path, keyPath))
    return;

  // ...
});

Thereby making the reference cursor dance a little silly.

@dashed
Copy link
Contributor

dashed commented Feb 7, 2015

Idea: Given a cursor, without an official public API to access the associated keyPath, we'd like to convert this into a reference cursor.

@mikaelbr
Copy link
Member

mikaelbr commented Feb 8, 2015

I'm thinking this is getting mature enough to squash and merge? Any objections, folks?

@skevy
Copy link

skevy commented Feb 8, 2015

Looks good to me!

@dashed
Copy link
Contributor

dashed commented Feb 8, 2015

Looks fine to me. I think later on, we should explore .on() or similar.

@mikaelbr
Copy link
Member

mikaelbr commented Feb 8, 2015

At some point we can add specific evented listeners (add, change, delete). I have some stashed code for it, but for now it's not critical for this change. This is something we potentially can build on.

…sors)

Adds possibility to "scope" down on a given path, and have a reference
to a given cursor which always returns the updated cursor. Also allows
for adding specified listeners for different paths:

`structure.reference(['some', 'path']).observe(function);`
mikaelbr added a commit that referenced this pull request Feb 8, 2015
Adds support for "reference cursors"
@mikaelbr mikaelbr merged commit 9d211da into master Feb 8, 2015
@skevy
Copy link

skevy commented Feb 8, 2015

Awesome job @mikaelbr.

@mikaelbr
Copy link
Member

mikaelbr commented Feb 8, 2015

Thanks! 🍰

@skevy
Copy link

skevy commented Feb 8, 2015

Now we just need docs :)

@mikaelbr
Copy link
Member

mikaelbr commented Feb 8, 2015

Indeed. Was just having the discussion with @torgeir . I'm going to open issues.

@skevy
Copy link

skevy commented Feb 8, 2015

K.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants