-
-
Notifications
You must be signed in to change notification settings - Fork 141
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
spike for prop notification #586
base: master
Are you sure you want to change the base?
spike for prop notification #586
Conversation
I see where you are going with this and it seems reasonable! Do you have any unanswered questions or blockers? |
One question: it's ok the |
@snewcomer Dug a little bit more on this today to try to fix the tests and found out a few things...
We traverse through proxies to find the nearest defined parent to notify at that level. At least with the current design, adding observers to the changeset should be added to One downside of this is that we won't get the changeset as Overall with this PR we get deep observers and specific prop notifications, playing well with most octane patterns and also classic. We could... to allow a more ergonomic api, – but I think it might do more harm than good – could potentially do this inside the proxy getter: export function Changeset(obj, validateFn = defaultValidatorFn, validationMap = {}, options = {}) {
const c = changeset(obj, validateFn, validationMap, options);
return new Proxy(c, {
get(targetBuffer, key, receiver) {
if(key.toString() === 'addObserver') {
return targetBuffer._content?.addObserver?.bind(targetBuffer._content)
}
if(key.toString() === 'removeObserver') {
return targetBuffer._content?.removeObserver?.bind(targetBuffer._content)
}
const res = targetBuffer.get(key.toString());
return res;
},
set(targetBuffer, key, value /*, receiver*/) {
targetBuffer.set(key.toString(), value);
return true;
},
});
} This would make that but using One problem with this approachWith this approach if the |
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 PR looks like it is making progress!
current = current[paths[i]]; | ||
} else { | ||
notifyPropertyChange(tryContent(current), paths[i]); | ||
} |
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 I'm understanding this correctly, we want to traverse down the object to the leaf key, calling notify property change as we go? Or do we want to reduce property notifications (as indicated in the issue) and only want to notify at the leaf key in paths
?
Two questions.
- Does the if else blocks need to be switched? If
!= undefined
, we want to notifyProperty change at that level? - Does it make more sense to start at the leave and traverse up? Or rather, start at the last key in
paths
and iterate backwards?
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.
Rewrote my last comment as my understanding changed.
The idea is to traverse down to find the deepest defined
ancestor.
example 1.
Take: key as foo.bar.baz
and data as { foo: { bar: {} } }
The output should: notifyPropertyChange(data.foo.bar, 'baz')
example 2.
Take: key as foo.bar.baz
and data as { }
, everything undefined
The output should: notifyPropertyChange(data, 'foo')
example 3.
Take: key as foo.bar.baz
and data as { foo: {} }
, so bar
undeefined
The output should: notifyPropertyChange(data.foo, 'bar')
I think this could be optimized like this:
for (i = 0; i < paths.length; ++i) {
if (current[paths[i]] != undefined) { // we can still go deep
current = current[paths[i]];
} else {
//we just found that its a dynamic set, so we notify here and stop, no point in going further.
notifyPropertyChange(tryContent(current), paths[i]);
return;
}
}
//We didn't stop, we haven't notified, and we went the deepest as possible, so we notify there.
notifyPropertyChange(tryContent(current), lastPath);
I think its ok to start from the top, because we can early notify and return
I'm using this with ember-m3, MegamorphicModels and seems to work great too |
OK, so dug a lot more today, ObjectTreeNode proxies return different references between proxy access if the prop doesn't exists inside And so we can't realiaby notify, and so I propose: We'll try to find the deepest ancestor which could be notified i.e the reference to the underlaying content is the same on each access, which means that for deeply "fully" dynamic paths, we can't go deep or specific at all as I wished. For now, for my bullet proof observer use case, I think I incline to use a more event oriented api and probably could be documented as a way to observe reliably about changes In the best scenario, if the content isn't dynamic at all (the content has all the structure in advance), it should be specific with the notifications. In the worst scenario, the content is fully dynamic, we will notify from the top. In the middle ground scenario, we traverse until we're out of CONTENT defined paths and notify there. I recognize this solution probably is not the best one, but could be a known limitation, unless some other approach comes by. Another "solution" could be to have some sort of |
b44e85e
to
a186905
Compare
assert.equal('Jack', res, 'observer fired when setting value some.really.nested.path'); | ||
changeset.rollback(); | ||
assert.equal(undefined, res, 'observer fired with the value some.really.nested.path was rollback to'); | ||
}); | ||
|
||
test('can update nested keys after rollback changes.', async function (assert) { | ||
let expectedResult = { |
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.
Do we have a test for this issue so we can close it out when merged?
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.
ping! Lmk what you think about adding this test. Lots of great work in this PR.
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.
sorry for the delay, I ran that repo, I think it has a bug on the example itself, it uses
@value={{not (changeset-get this.changesetObj contextKey)}}
@onToggle={{action (changeset-set this.changesetObj contextKey) (not (changeset-get this.changesetObj contextKey))}}
I think that action helper is actually(?) mutating the CONTENT, somehow(?) and thus it actually have changes
but I just ran that branch with a few changes to the example and its working!
@value={{changeset-get this.changesetObj contextKey}}
@onToggle={{changeset-set
this.changesetObj
contextKey
(not (changeset-get this.changesetObj contextKey))
}}
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.
And so, I think... no further test need to be added? 🤔 I mean there are a few tests already checking for changes emptied after setting back the initial wrapped value? gonan double check
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 ran this again and the bug still exists, but I'm unable to track why it seems somehow sets
are applied to the this[CONTENT] without executing the changeset using changeset-set
.
…to spike-fix-prop-notification
Had to bump node because one dep needs >=12 now? it prompted because of me using error ansi-regex@6.0.0: The engine "node" is incompatible with this module. Expected version ">=12". Got "10.22.0"
error Found incompatible module. |
@@ -75,12 +75,12 @@ | |||
"release-it-lerna-changelog": "^1.0.3" | |||
}, | |||
"engines": { | |||
"node": "10.* || >= 12.*" | |||
"node": ">= 12.*" |
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 will be a major version bump I believe. What was the specific error? I thought v10 had another 6 days until EOL.
addon/index.js
Outdated
|
||
for (i = 0; i < paths.length; ++i) { | ||
const curr = current[paths[i]]; | ||
if (existsInContent && curr && curr.content === curr.content) { |
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.
curr
will fail for falsey
values, correct?
Do we want to use getPrototypeOf
and walk the chain checking for getOwnPropertyDescriptor
? Or perhaps we just directly check hasOwnProperty
on the object.
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, I think that makes sense
I noticed assigning curr=path[i] was triggering But in my sandbox: https://github.com/betocantu93/changeset-tests Stumbled with another bug, when you try to Not sure how that should be fixed over there, any suggestion? |
if (existsInContent && curr && curr.content === curr.content) { | ||
current = current[paths[i]]; | ||
const curr = safeGet(getContent(current), paths[i]); | ||
if (existsInContent && curr) { |
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 this fail if curr
is falsey?
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.
ping!
@betocantu93 Yes a fix over in validated-changeset with a simple test case seems prudent! Would happily accept. |
@@ -32,6 +32,33 @@ function maybeUnwrapProxy(o) { | |||
return isProxy(o) ? maybeUnwrapProxy(safeGet(o, 'content')) : o; | |||
} | |||
|
|||
function getContent(ctx) { | |||
return ctx._content ? ctx._content : ctx.content ? ctx.content : ctx; |
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.
Is _content
meant to capture from the Changeset and content
from an ObjectProxyNode?
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, that's correct.
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.
Sry for stringing this along but I do want to get a solution in. I'm learning from your work to understand the issue better.
So is it best to say, not only does notifyPropertyChange not work for nested keys (this is obvious to me), but also we are notifying on the wrong object? Meaning notifyPropertyChange(this, key)
leads to the wrong/no expected behaviour as well? I wouldn't have through this would have work for an ObjectProxyNode (non Ember object, etc)
Two more issues on my mind:
Not exactly sure how to handle that case inside the
What do you think about the second point? would it make this approach non ideal? My counter proposal is to have a tracked mirror/fake structure that we will be notified and (deeply) setted just for observavility purposes, instead of notifying the content. We just have to be sure (I think 🤔) that we consume tags per get/set, with that octane getters should work and classic computeds should work, and for |
Fixing 1 in #590! Thanks! Regarding 2, it sounds like we have to get really really granular on our KVO notifications. Just for some clarification, is the problem solely with Overall, we do need to get this right as you have pointed out - |
thanks for the fix, it was so simple!, I thought some sorcery would be needed haha.
I think Ember I guess we could add a custom addObserver. pseudo: //Ember-changeset
addObserver(path, callback) {
setDeep(this.mirror, path, true); //We actually dont care about the value, we just need this "notifiable" structure
addDeepObserver(this.mirror, path, callback); //add the observer to the ancestor
} |
I was thinking in something like this in pseudocode: //ember-changeset
new Proxy(obj, {
get(target, path) {
setDeep(target[MIRROR], path, true);
.... continue default
},
set(target, path, value) {
setDeep(target[MIRROR], path, value /*I think we don't care about the value*/);
....continue default
}
})
setProperty(){
....
deepNotifyPropertyChange(this[MIRROR], path)
} We are basically building lazily this notifiable pojo, not entirely sure if it's compatible with native getters and computeds, or if the mirror should be marked as tracked and consume the tags per set/get, so when notified, native "octane" getters should recompute, but for classic computeds probably mirror dependencies should be manually added. |
…to spike-fix-prop-notification
How does the |
Hello, this PR seems to fix a few bugs. (not sure if its the right way).
It seems that notifyPropertyChange doesn't work for nested paths, so this PR runs notifyPropertyChange on deepest defined ancestor also ensures to always
set
, so it can return to initial value, which is the bug described in #585 but making sure it cleans the changes if the oldValue is equal to the newValue