-
Notifications
You must be signed in to change notification settings - Fork 21
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
Conversation
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 |
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 |
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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) { |
There was a problem hiding this comment.
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();
}
};
}
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
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 |
@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. |
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 |
Why can't we make the function subscription a little bit smarter? We track by reference counting. We subscribe
All we want and care about is adding and removing functions via |
return original.apply(this, arguments); | ||
} | ||
}, | ||
cursor: function () { |
There was a problem hiding this comment.
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.
I think Imagine having Maybe |
@mikaelbr looking good so far 👍 |
Since |
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. |
We had a discussion on this, but we found that
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); |
There was a problem hiding this comment.
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
.
// 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 |
This may require to rename |
unsubscribeAll: function () { | ||
cursor = void 0; | ||
if (!self._pathListeners[pathId]) { | ||
referenceListeners = []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be referenceListeners = [changeListener];
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nevermind.. ignore above comment.
See: https://github.com/omniscientjs/immstruct/pull/30/files#r24289215
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 |
referenceListeners = void 0; | ||
cursor = void 0; | ||
|
||
this.onChange = void 0; |
There was a problem hiding this comment.
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.
Throwing this idea in. Since there is a |
@@ -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]; |
There was a problem hiding this comment.
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]
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 😄
There was a problem hiding this comment.
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.
See: https://gitter.im/omniscientjs/omniscient?at=54d621c1c4386fab2709bf4c |
self._pathListeners[pathId].splice(fnIndex, 1); | ||
}; | ||
}, | ||
cursor: function () { |
There was a problem hiding this comment.
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);
},
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
This is awesome that this is being worked on! The only thing I would say with this is that the API for |
I'm throwing around this idea: https://gitter.im/omniscientjs/omniscient?at=54d6273cdb625d410e7c26bf Usage story:
|
@dashed you don't think using an EventEmitter and |
@skevy An EventEmitter is already used indirectly here: Lines 39 to 44 in 8d7eba1
To elaborate, We add features to an everlasting cursor; such as observation. Through the everlasting cursor, we can 'observe' the internal cursor. That's why |
This may be a good argument against At the moment, structure.on('swap', (newData, oldData ,path) => {
if(!equalPath(path, keyPath))
return;
// ...
}); Thereby making the reference cursor dance a little silly. |
Idea: Given a cursor, without an official public API to access the associated |
I'm thinking this is getting mature enough to squash and merge? Any objections, folks? |
Looks good to me! |
Looks fine to me. I think later on, we should explore |
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);`
Adds support for "reference cursors"
Awesome job @mikaelbr. |
Thanks! 🍰 |
Now we just need docs :) |
Indeed. Was just having the discussion with @torgeir . I'm going to open issues. |
K. |
A PR is easier for cooperation