-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
[FEATURE ember-metal-injected-properties] Injected properties #5162
Conversation
throw new EmberError("Cannot set injected property '" + keyName + "' on object: " + inspect(obj)); | ||
}; | ||
|
||
InjectedPropertyPrototype.teardown = function(obj, keyName) { |
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.
Would this work instead?
ComputedProperty.prototype.teardown.call(this, obj, keyName);
My thoughts were basically, just to ensure the same things that were setup by calling the ComputedProperty
constructor are torn down in the future. Currently, what this has is perfectly fine, but to stay in sync I think maybe we should use the upstream teardown
...
Looks pretty good on first review. Will dig a little deeper and answer some of your questions in the intro later tonight or tomorrow. |
@method _verifyInjectionDependencies | ||
*/ | ||
_verifyInjectionDependencies: on('init', function() { | ||
var descs = meta(this).descs; |
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 code already exists in the container, can we instead have it scope up these requirements, and validate there?
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.
additionally, for internals we do not use on('init') instead we place the method explicitly in the constructor to ensure ordering
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.
as I think about this, this validations do not occur soon enough, as they occur after the users init
hook, leaving these property injections potentially invalid for constructor use.
I believe the only way to fix this is to address my above concern
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 that only leaves modifying the constructor of CoreObject
, correct? Also, what exactly did you mean by "scope up these requirements" (let the container throw the exception when the lookup fails?)
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 exactly. Note this will likely require changes to the container itself.
this also addresses: "Referencing the container in ember-metal" as injections and validations would only occur if the entity in question was instantiated by the container.
also, good job on this PR. 🍻 |
you usage of |
Awesome, thanks! I addressed the super constructor and |
@slindberg awesome, this is great work! Keep me posted, i'll provide feedback as time permits. |
Nice job. I need to review in closer detail but this is very promising. |
@stefanpenner Looking at the container module it looks like you were referring to the logic in
The current implementation tries to stay hands-off the container API and therefore away from number the third option (see the "Injection" section in the PR comment), mostly because creating a way for objects to communicate with the container seemed like a bad idea. Maybe it's not though, and establishing some kind of 'needs' dependency specification API for all objects in lieu of constructor parameters isn't a bad thing. Note: I'm pretty sure the current Short of going the third route, I don't see how this could be accomplished:
Thoughts? (Thanks for the quick feedback already!) |
this.type = type; | ||
this.name = name; | ||
|
||
this._superConstructor(function(keyName) { |
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._super$Constructor
$
makes it clear that this is an internal non-colliding thing. In theory this replicates what es6 super
would do
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 looked through the codebase for that pattern and didn't find anything. I guess I just didn't want to be the first one to do it 😛
👍 (3.) is partially aligned and partially not. We only want to use the container for validation at time of object instantiation, but the actual lookups should happen lazily. This gives us the nice fail-fast validation of constructor injections, but while enabling lazy lookup and class level annotation. |
I may be wrong, but i really feel this validation is not CoreObject's concern at all, but only when an object is instantiated via the container should it be a concern. This enables very simple unit testing for example: export default Ember.Controller.extend({
twitter: Ember.inject.service()
}) in unit tests can very simply just instantiate by providing collaborators at construction: var controller = Controller.create({
twitter: twitterCollaborator
}) In normal app usage, via injection (or lookup) the container does what the container is good at, managing dependencies and as such the validations for lazy dependencies. |
@stefanpenner I refactored the instantiation-time validation to happen in the container. Modified the container slightly by adding a Still feels a little weird for the container to be aware of injections and not do the injecting itself. Let me know what you think. |
@slindberg This is great, thanks! I'm a little concerned about having the validation mixin applied to all objects- is that overkill? Does it make sense to scope that mixin onto routes, controllers, and services for now? Would like your thoughts on it. @stefanpenner Can you say more about why you want lazy lookup here? I see it helping to avoid an up-front performance hit from looking up the dependency chain of an object eagerly, but I'm not sure that would often be a real world challenge. I'm happy with it being lazy, but would like to know if there is a specific justification. |
|
@mixonic In order to be able to enforce that, for instance, a controller cannot be injected into a model, the validation step has to be performed at mixin time of the model, which means the model class must have the Currently the validation functions only perform assertions, and so they are no-ops in production except that all supporting overhead would still be there. One solution is to just convert the asserts into real run-time errors, which I'm conflicted about. Another is to potentially defeaturify more than just the asserts, and do away with the validation mixin in production entirely, which I don't think is currently possible (note that this would also make @mixonic, @stefanpenner In regards to the perf case, I'm beginning to think that there's not much to be gained from lazy injections given the current uses (controllers and services). Controllers in the current route chain are nearly always already fully looked up by the router before the controllers get a chance to use them, so delaying lookup doesn't provide much benefit. And since the nature of services is that they are singletons and small in number, if they exist in the app they are likely to be looked up during its lifecycle at least once anyway, so eager lookup is fine (although perhaps not within a highly complex and segmented app). I am also assuming that there isn't a terrible amount of overhead for factory injections in the container itself, which there doesn't appear to be. Thoughts? |
@slindberg the cycle case is very common, its not just 1 step removed cycles but in larger apps cross dependencies (although maybe not ideal) across many steps are very common and easily solvable by laziness. Additionally the cost of instantiating a large number of dependencies that wont be used till after boot is also concerning. I am bullish on laziness for this feature. |
I also favor the laziness. Pondered it last night, I just wanted to see Stef's arguments to back up my own. @slindberg gotcha re: the object-level mixin. It is definitely super unfortunate. Trying to think of something nicer, but I see what you are talking about. |
@stefanpenner makes sense. Re a real-world example of these cycles: an app that has a dense controller dependency graph, so eager injection would end up instantiating a large number of potentially unneeded controllers at app boot. If I understand correctly, this definitely should be avoided. It should also be possible to mark these injections as eager on a case-by-case basis for people who want to tune their app performance where it makes sense. Did you get a chance to look at my container validation changes? |
@slindberg im interested in "eager" vs "lazy" but i suspect we can get away with always lazy. |
@slindberg I'm going to make time this evening to thoroughly review the current implementation :) |
@stefanpenner thanks 😃 I'm on no schedule for this feature, I'm mostly just excited to be able to contribute to Ember! |
|
||
for (var i = 0, length = injections.length; i < length; i++) { | ||
injection = injections[i]; | ||
fullName = typeof injection === 'string' ? injection : injection.fullName; |
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 it possible to ensure this transform isn't needed at this point? Can injections already be parsed and normalized?
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.
Def a little weird. Updated to change the lazyInjections
hook to return a hash that is normalized and then passed to validateInjections
.
@slindberg This is great! 🍻 Regarding "Referencing the container in ember-metal"... It's fine to define the descriptor in ember-runtime instead. I agree that this descriptor is really about working with containers, which isn't inherently a property of ember-metal. |
If injections are intended to be used on a DS.Model, something similar to 05143e6 may need to be added depending on how soon this is merged. |
@slindberg Do you think you'll have an opportunity to update this PR to address @stefanpenner's concerns above? I would LOVE to get this shipped in the next release. |
@tomdale I'm happy to create a new PR, but @stefanpenner's concerns were never fully clear to me (otherwise I would have tried to address them before this was merged). Let's make sure we're all on the same page. To clarify the current state of injected property validation, there are two types of validation that occur:
Reading over @stefanpenner's concerns:
and
It sounds like (2.) container validation as implemented does not need to be changed, and that (1.) type validation should happen in the container's The other option is of course to get rid of type validation altogether, which may make sense given that controllers are going to be replaced with routeable components in the near future. Please let me know how to proceed on this. |
The PR is close, but my concerns are as follows: Currently it is possible to satisfy the lazyInjection between the time that the factory is created and the time it's first instance instantiates. I don't believe this window is needed and if used will very likely by brittle. I would like to close this window. Additionally the current implementation re validates lazyInjections on each object instantiation. This is wasteful as the fate should already be known at the time the factory is created. My recommendation, although I am open to other suggestions, would be to validate the lazy injections when the factory is first created and but before it is cached. Likely here and here It is possible I am overlooking some detail that makes this not possible, but I believe it is sound and will yield the best possible solution. If problems do exist let me know and I will gladly provide more suitable advice. |
var normalizeInjectionsHash = function(hash) { | ||
var injections = []; | ||
|
||
for (var key in hash) { |
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 hash
is always Object.create(null)
we can mitigate the slow hasOwnProperty
check here.
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 me, this is a question of the degree to which the container should be considered a separate microlib. AFAICT it currently maintains reasonably loose coupling with the rest of Ember, so I hesitate to remove this check even though its safety could be internally guaranteed. If I had to make the call, I would keep the check in at the slight perf cost. Do you disagree?
This is also related to whether the lazyInjections
method on ember objects should be considered public or not.
@stefanpenner thanks for all the feedback, I'll open a new PR to address your concerns. However, you haven't weighed in on what I called 'type validations'. How important is it to enforce that controllers are not injected into non-controllers? |
@slindberg that part of the request is a @tomdale + @wycats high level concern they might be best to respond to that not me. I do really like the |
Also, after reviewing this again, I wonder (and I hope we can, but maybe we can't) close that window between factory creation and first instantiation. This is what I know for sure:
This is what I am unsure about at this moment:
export default {
name: 'thing',
initialize(container, application) {
container.lookupFactory('something:else').reopen({
foo: Ember.inject(somethingValid)
});
}
}; Now it is technically possible to reopen after the first instantiation, but this is honestly the root of many ember performance issues, gnarly bugs, and anxiety induced receding hairlines and as such I would love to no longer cater to it. |
@stefanpenner Do people really reopen factories they've retrieved from the container? I'd think you'd just reopen the base class. |
I think no matter how classes are reopened, there's always the potential for container lazy lookup validation to become stale (e.g. one initializer runs and looks up After using my co-worker as a rubber duck for a solid 15 minutes, I can think of two sane paths forward:
The second option allows classes to be reopened after container lookup and avoids making the container aware of even more model internals, which I'm personally in favor of. However deprecating I'll be working on updates on this branch, and when we've figured out how I should address the above issues I'll open a new PR. |
@stefanpenner @tomdale ping |
It seems like the window we want to enable should leave open factoryLookup -> create, but post create we don't care anymore. So the validation should occur (but be cached) on first instantiation. Logging or asserting seems fine, i believe we can support this case but I REALLY WANT IT TO DIE... Reopens post first instance creation costs us much pain and I will be advocating we kill it in the future. |
@stefanpenner Sounds good, I'll get to work on caching the lazy lookup validation on first instantiation. For anyone curious, I was surprised to find out that reopening a factory registered with the container seems to have no effect on the factory or any instances of it created before or after reopening: jsbin. This is rather convenient for the purposes of validating injected properties, since it's already not possible to reopen after instantiation. |
Concerns addressed in #9760 |
@slindberg This is an awesome feature! Thank you! 😄 During a refactor I've moved many things from my home-made services structure into Ember Services. However, for some use-cases I'd like to use something different, and it would be neat to create custom injections. So, my question, do you know why I can see the downside of people using it incorrectly, but that can be handled using documentation. Ember does surface quite a bit of useful low-level functionality in other areas (e.g. around the run-loop). Would be neat if |
The concern boiled down to allowing arbitrary instance <-> instance injections. This confuses the data-flow model. We feel good about allowing service -> component direct communication (since the life-cycles enable it) but not very good about aribtraryThingWithItsOwnLifeCycle < otherArbitraryThingWithItsOwnLifeCycle. I think the easiest way to illustrate the limitation, is to align it slightly with the bindings down actions up strategy. |
Got it. I'll respect whatever you prefer — i.e. it's a bad idea to make a change to Ember if I'm the only one with a need for it :) But count this as one token in the make-it-public bucket. I've got one use-case where this would be useful, and still wouldn't collide with the limitation above (but it's narrow, not generalizable). And I think the limitation could also be handled by documenting clearly the intended usage. I'll find a work-around for my case (but if many others start asking for this it may be worth revisiting this). Upgrading our app to 1.13 and a more component-driven structure. Works splendid so far! You [the core team] has done an amazing job! ⛵ |
Share our usecase :) the limitation is really conservative today. |
I would like to create an injection helper for a pattern called 'actions', which are functions that do some business logic: For example, in export default function (comment, file) {
let store = this.get(comment, 'store');
return file.upload().then(function (data) {
let attachment = store.createRecord('attachment', {
comment: comment,
url: data.headers.Location
});
return attachment.save();
});
} This nicely encapsulates somewhat complicated behavior into a function. In our route, I might just care about testing that the action got called. In that case, it would be nice to swap out the action for a mock that returned a promise that I had control over in the test. That way, I could say that:
This also removes complications around testing these usually tricky bits of business logic that cause a lot of friction to test. |
@tim-evans I have had this issue as well. I've wanted to inject a function, but found myself having to create a service with a single method on it—and having to do a little song and dance just to expose one method felt very Java-like to me (in the bad way). I for one would definitely be open to an RFC explaining the primitive and making it possible for folks to inject functions. |
FWIW: it is possible today with something like: export default { |
But I absolutely agree that we should make it more ergonomic (and think through the ramifications) via an RFC. |
Injected Properties
This PR is intended to implement the necessary features for Ember 'services' (see #4861). It introduces 'injection helpers' which are computed properties that are container-aware, but whose use is type dependent (e.g., you cannot define an injected controller property on a view). As discussed on @tomdale's PR, there is ambiguity between an injection helper named
Ember.controller
and the actualEmber.Controller
class, so to avoid this I've created a new top level namespace calledinject
(see below).As an example, the following is enabled:
Which is functionally equivalent to:
This PR also adds the
controller
andservice
injection helpers (as well asEmber.Service
), but those commits can easily be extracted into another PRValidation
In order to enforce good practices, injection helpers can only be used with certain ember objects (or rather can't be used with certain ember objects).
services cannot be injected into views or componentsThis validation check can happen in one of two places:
The first option seems better since it happens early and only once.
A second type of validation ensures that the object with injection properties has a reference to the container, and that the container has knowledge of the requested injections (through
app.register
or the resolver). This check can happen at one of two places:The first option seems better since it happens earlier (assuming that registration of container types happens during app bootstrap).
Injection
When an injection helper property is declared on a class, all instances of that class need the property looked up and set. There are a few places this can happen:
CoreObject
The first option would maintain consistency with current container injections and allow instances to access their properties without calling
.get()
, but require either accessing the injection at mixin time (where a container is not currently accessible), or by altering the container to be aware of the ember object model (which it is currently only vaguely aware of). The last approach has the advantage of not performing a lookup if never accessed, and being able to take advantage of CP cache, which seems to make it the best candidate.Design Decisions
Feature name
This touches more than just
ember-metal
, but that package is the lowest common denominator. It would be trivial to split this up into multiple dependent features as an alternative.inject
namespaceThis is simply to avoid the potential ambiguity between
Ember.controller
/Ember.Controller
Ember.service
/Ember.Service
. It also happens to read well, since what the developer wants to do is, for example, 'inject [a] controller'.inject
namespace (not) as a functionMuch like
Ember.run
andEmber.computed
, theEmber.inject
namespace could potentially be a convenience method for a container injection property, however that would enable the "injection of anything into anything" anti-pattern that should be avoided if possible. By only allowing injected properties through predefined methods, Ember is enforcing best practices.Referencing the container in ember-metal
The container is not mentioned anywhere in
ember-metal
, but since injected properties are type of descriptor, and all other descriptor properties are defined inember-metal
, it seemed like the best place. It could also easily go in theember-runtime
package.Validation in(no longer applies)Ember.Object
init
eventThere are a few places instantiation-time validation can happen:
CoreObject
's constructorObject
's init` event/hookObject
'sdidDefineProperty
hookAlthough nothing in Ember core (to my knowledge) currently uses the
init
event, it seemed like the best option since requiring all existing subclasses that defineinit
ordidDefineProperty
hooks call_super
for validation to work, is IMHO dangerous and could be considered a breaking API change. Yes, controllers currently implement this check in theinit
hook, but extending it to all objects is much farther reaching.willMergeMixin
onEmber.Object
This is obviously dubious for the potential performance hit. However, during my
ember-metal
spelunking, it seems like this private hook is specifically intended for doing any special mixin-time handling. So short of baking injected property validation intoember-metal
itself, this is all I could come up with.Branding vs.
instanceof
This is something that @stefanpenner mentioned in the original PR. I could not find much information on the performance implications of "branding" versus use of the
instanceof
operator, except by inference in this jsperf (which only shows results in chrome). This did not seem to warrant breaking existing patterns, and can easily be changed later when otherinstanceof
checks are converted.Ember.assert
vs.throw new EmberError(...)
To be perfectly honest, I have no idea when something warrants a full production-time exception over a development assertion. Is there a guide for this anywhere? I ended up just making best-guesses.
TODO
It should be possible to cache a list of injected properties on the constructor's meta object when mixin-time validation happens, thereby avoiding the repeated descriptor lookups at instantiation-time validation. I just don't know enough about how metadata is stored/accessed to be confident enough to do this, especially since there are additional considerations when classes get extended multiple times with new injected properties.