-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Upgraded IdentityMap and RecordMap #4664
Conversation
cc @stefanpenner got the first big part done, should have this PR done by EOD, hoping to at least have started the relationship PR that needs to accompany. Would like thoughts on the new location of |
for (let i = 0; i < records.length; i++) { | ||
record = records[i]; | ||
record.unloadRecord(); | ||
record.destroy(); // maybe within unloadRecord |
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 copied this over from the existing unloadAll functionality, but I suspect this is actually buggy an we want to reset the length of this._records to 0
here. I left it as is with the guess that unloadRecord
removes it from the array on it's own (which would also be why we slice).
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.
ya, I think (but we should double check with someone else) we want to set this._records.length = 0
just after let records = this._records.slice();
store._push
story.store._push
story.
} | ||
|
||
/* deprecated way of accessing modelClass */ | ||
get type() { |
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.
we still use "type" enough places that I haven't pulled the trigger on an actual deprecation notice yet, want to keep this quieter until we're closer to the end of internals cleanup.
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 may be a bit zalgo, as if there is no modelClass
via type
call, this will be null.
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.
Wont the getter for modelClass
lazily lookup the model class or is there something I'm missing?
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.
@bmac I think the issue raised is that if there is (literally) no modelClass to resolve, that it will always be null, and ergo we will perpetually keep trying to resolve it with every "get" of type or modelClass.
I am excited about this, but I would love to review this thoroughly, although I may not have cycles until tomorrow or sat. But will try for sooner |
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 on-board with this direction, left some thoughts some of which should be addressed. Feel free to ping me if you want to discuss more.
} | ||
|
||
destroy() { | ||
this._store = null; |
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.
we should add the conventional isDestroying isDestroyed fields, so when debugging we can quickly see what is up.
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.
👍
return null; | ||
} | ||
return this.store.modelFor(this.modelName); | ||
}), |
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.
readOnly
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.
👍
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.
resolved
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.
@stefanpenner making this change made our tests fail :'(
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.
for good reasons, or test bugs?
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.
Fixed
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 doesn't do what you think it is. (the last arg is dropped, but i suspect we want it to actually run)
I suspect you want:
type: computed('modelName', function() {
// ...
}).readOnly()
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.
Indeed, wanted this.
|
||
return typeMap; | ||
recordMap = new ClassMeta(this, modelName); | ||
recordMaps[modelName] = recordMap; |
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 this be an argument to ClassMeta
?
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 originally was going to pass in owner
, but wanted to piggy-pack on the modelFor
behavior (for now). I think longer term modelFor
would move into this object itself.
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.
seems good.
let id = internalModel.id; | ||
|
||
internalModel.updateRecordArrays(); | ||
|
||
if (id) { | ||
delete typeMap.idToRecord[id]; | ||
delete recordMap.idToRecord[id]; |
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 we add removeRecordById
to classMeta
/ recordMap
?
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.
} | ||
|
||
let loc = typeMap.records.indexOf(internalModel); | ||
typeMap.records.splice(loc, 1); | ||
let loc = recordMap.records.indexOf(internalModel); |
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 we this also be pushed into classMeta / recordMap
rather then exposing the internals?
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.
} | ||
|
||
get metadata() { | ||
return this._metadata || (this._metadata = new EmptyObject()); |
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 may also be like above
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.
unlike above, the way this one works we don't deal with an "what if always resolves to null?" situation.
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.
so its never inspected for empty-ness, always to append/mutate?
} | ||
|
||
/* deprecated way of accessing modelClass */ | ||
get type() { |
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 may be a bit zalgo, as if there is no modelClass
via type
call, this will be null.
for (let i = 0; i < records.length; i++) { | ||
record = records[i]; | ||
record.unloadRecord(); | ||
record.destroy(); // maybe within unloadRecord |
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.
ya, I think (but we should double check with someone else) we want to set this._records.length = 0
just after let records = this._records.slice();
@@ -104,13 +104,12 @@ const { | |||
@class InternalModel | |||
*/ | |||
export default class InternalModel { | |||
constructor(modelClass, id, store, data) { | |||
constructor(modelName, id, store, data) { | |||
heimdall.increment(new_InternalModel); |
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 the internalModel have a direct reference to its classMeta
/ internalClass
, that way get modelClass()
can simply delegate to it.
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 agree but wanted to limit the scope of the initial 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.
sounds reasonable
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.
resolved
getReference: function(type, id) { | ||
return this._internalModelForId(type, id).recordReference; | ||
getReference(modelName, id) { | ||
modelName = this._classKeyFor(modelName); |
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.
override argument value makes debugging annoying
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.
Agreed, hadn't thought about that, will refactor to use a new variable name. I'd also like to bring up (as a separate PR) that it would be nice to officially deprecate support for camelCase modelName usage.
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.
seems fine, as long as we don't forget
@runspired given this PR, what remains re: loading modelClass spuriously during cache load? |
@emberjs/ember-data-contributors we would love more sets of eyes on this, we don't want to unilaterally sneak something like this in. |
I'm 👍 on the direction of this change. |
@stefanpenner with these changes, you no longer load a modelClass for anything not related to
Iteration currently only happens during create/update/delete data flows, so this means that for what we wanted as a first pass this achieves that. |
@runspired nice, have you tested this in our app. I want to make sure we didn't forget anything obviously for those offending code-paths. |
Not surprisingly, this does seem to break some ember-data-model-fragment tests. I'll look into getting that fixed once this lands. One question though, you said there is also a coming PR for part two of this effort. Do you plan to squeeze that into the same ember-data release? If so, I'll wait to investigate until the effort is complete. Edit: There are also some other breaking tests that seem related to serializer changes for model-fragments. Just looking for the dust to settle before I dive in. |
@workmanw is there an effort to get model-fragments using public APIs and/or derive the needed public API's for model-fragments to be successful? We would love to not cause you extra work, but the intimacy between the two projects appears quite fragile. |
@stefanpenner Sorry I missed your response. To be perfectly honest, I wouldn't even know where to begin. I don't have a commanding knowledge of the ember-data internals. I'm not the only maintainer nor the original author, so I can't speak for everyone, but I would be open to trying to make this more durable. The biggest challenges we have in ember-data-model-fragments are 1) we interact heavily with the internal model and 2) we have to monkey patch part of the ember-data public API. For example, we override If you have idea's on how ember-data's API could be evolved to help facilitate this, I'd love to hear them. |
@workmanw a good start is thoroughly documenting all the things that require private API usage (and why). That way those more familiar with ember-data can help with what would need to change. |
c3dbc42
to
c54e7a1
Compare
@runspired let me know when/if you ready for another code-review |
store._push
story.aa86e09
to
df21a7b
Compare
|
||
/* deprecated way of accessing modelClass */ | ||
get type() { | ||
throw new Error('RecordMap.type is no longer available'); |
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.
historically we've had type
stashed away, but @igorT was skeptical it should have been there. It is "nice to have" but we should think it through more carefully. I added this error just to see if anything in the codebase depended on it being there, it doesn't appear that anything did!
a89c7ee
to
22971f9
Compare
@method clear | ||
*/ | ||
clear() { | ||
let recordMaps = this._map; |
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 need to actually clear these, our should we just "release" the dominator?
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 believe we must specifically clear them because clear is responsible for tearing down the internal models. This is a refactor that preserves existing logic for teardown, but it is absolutely possible we were doing an excessive amount of work on teardown in the past.
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.
ah ok, lets punt.
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.
@stefanpenner all but 2 tests still pass with use simply releasing, and they look to be two tests that are testing whether internal-model teardown things are done.
return null; | ||
} | ||
return this.store.modelFor(this.modelName); | ||
}), |
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 doesn't do what you think it is. (the last arg is dropped, but i suspect we want it to actually run)
I suspect you want:
type: computed('modelName', function() {
// ...
}).readOnly()
@method clear | ||
*/ | ||
clear() { | ||
let recordMaps = this._map; |
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.
ah ok, lets punt.
This looks good to me @runspired. Do you mind rebasing it when you have a chance? |
… of guid as the class identifier for recordMap
937d241
to
fe74d89
Compare
@bmac rebased! |
When pushing records into the store (no materialization), we only needed to load the associated modelClass for "key" purposes. This PR shifts ember-data to consistently using a normalized
modelName
as the key, making it possible to push internal models, assemble record arrays, and (potentially in the future) even make requests without any access to the modelClass.Step two of this effort (a separate PR) will be to enable the parsing of
modelClass
to avoid needing to load the classes for related models. Combined, this gives us a happy story for initial render in apps with pre-loaded data and complicated relationship graphs.In addition, this paved an easier path forward for #4584
refactor typeMap => recordMap
refactor to using modelName instead of guid as the class identifier for recordMap.
refactor InternalModel to not need
modelClass
at instantiation.remove calls to
modelFor
where now possibleremove
typeMapFor
consider deprecation notices for
InternalModel
andbuildInternalModel
in case private APIs were abusing them and now need to refactor frommodelClass
tomodelName
.(will be separate PR, but needed to complete this effort) refactor relationship setup to not need to lookup the related modelClass to prevent "pulling the string" on the entire graph.
UPDATES
I believe this is ready to ship but want to document a few things:
ClassMeta
is not unit tested (yet) but it is implicitly tested as it exposes an identical interface to the pojo that was ourTypeMap
before.store
tonew ClassMeta(store)
instead ofowner
in order to support the ability for it to resolve themodelClass
viastore.modelFor
. Longer term this should beowner
store.modelFor
should instead useclassMeta.modelClass
to resolve the modelClass if necessary._push
does not requiremodelClass
at all. The relationship PR is related to this "lazy push" story but is not required for it.