-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
Distinguish installing a service and creating it #7839
Conversation
src/service.js
Outdated
: opt_factory(context); | ||
// The service may have been requested already, in which case we have a | ||
// pending promise we need to fulfill. | ||
if (s.resolve) { |
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.
Won't we need to resolve regardless of s.ctor
?
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. Done.
src/service.js
Outdated
const ctor = s.ctor; | ||
s.obj = new ctor(context); | ||
} else { | ||
// TODO(kmh287): Replace opt_constructor param with ctor on service |
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 this a legacy path while we convert over?
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. Eventually the opt_constructor argument can be removed entirely, simplifying this a bit
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.
Good catch! OOC, which services have getServicePromise
called before registration?
src/runtime.js
Outdated
@@ -507,13 +506,15 @@ function prepareAndRegisterServiceForDocShadowMode(global, extensions, | |||
* @param {function(!./service/ampdoc-impl.AmpDoc):!Object=} opt_factory | |||
*/ | |||
function registerServiceForDoc(ampdoc, name, opt_ctor, opt_factory) { | |||
dev().assert((opt_ctor || opt_factory) && (!opt_ctor || !opt_factory), | |||
dev().assert(!!opt_ctor != !!opt_factory, |
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.
Nit: Single negation would suffice.
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.
Done.
src/runtime.js
Outdated
registerServiceBuilderForDoc(ampdoc, | ||
name, | ||
opt_factory || undefined, | ||
opt_ctor || undefined, |
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.
|| undefined
is unnecessary.
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.
Done.
src/service.js
Outdated
*/ | ||
function registerServiceInternal(holder, context, id, opt_factory, opt_ctor) { | ||
dev().assert(!!opt_factory != !!opt_ctor, | ||
`Provide a constructor or a factory, but not both for service ${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.
Nit: +2 spaces
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.
Done.
src/service.js
Outdated
`Provide a constructor or a factory, but not both for service ${id}`); | ||
const services = getServices(holder); | ||
let s = services[id]; | ||
if (s && !s.obj && !s.ctor && !s.factory) { |
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.
Long chains of logical ops are not very readable. I'd rewrite this like:
if (!s) {
s = services[id] = ...
}
s.ctor = s.ctor || opt_ctor;
s.factory = s.factory || opt_factory;
The above code won't overwrite existing ctor/factory props. I recall you mention checking for that case?
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 is a slight difference here. The proposed code would allow a constructor or factory to be registered even if the other type of creator already exists.
For instance:
registerService(window, 'Foo', /* constructor */ undefined, window => new FooClass1(window));
registerService(window, 'Foo', FooClass2);
getService(window, 'Foo')
The last line will return an instance of FooClass2, when it should return an instance of FooClass1. Reversing the lines will still result in an instance of FooClass2 because when we instantiate the service, we prefer a constructor to a factory.
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.
True, but that should be fine. Besides, we shouldn't implement this method based on the implementation details of getService
.
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 offline.
src/service.js
Outdated
return s.promise = Promise.resolve(s.obj); | ||
} else if (!s.ctor && !s.factory) { |
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.
Nit: IMO this is more readable if you remove this branch and put the condition into the assert()
. This conditional implies that ctor
and factory
are relevant to the implementation of this method when they aren't really.
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.
Sure.
src/service.js
Outdated
const holder = getAmpdocServiceHolder(ampdoc); | ||
registerServiceInternal(holder, ampdoc, id, opt_factory, opt_constructor); | ||
if (opt_instantiate) { | ||
getServiceInternal(holder, ampdoc, 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.
Shouldn't this also be called if getServicePromise
has already been called for this service?
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 so. If the caller of registerServiceBuilder chose to only register and not instantiate the service, then I'm not sure we should instantiate it here if there's an existing service promise. What are your thoughts?
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 call getServicePromise
on service foo
and then register it afterwards, when will the promise be resolved?
Isn't the point of this issue to not instantiate the service until requested? Does getServicePromise
not count?
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.
Before this change, getServicePromise returned a promise that didn't resolve until another caller created the service.
In your example, the promise won't be resolved until getService(window, 'foo') is called.
@choumx viewer, storage, amp-analytics-instrumentation are all services that are requested via getServicePromiseForDoc. |
src/runtime.js
Outdated
@@ -507,13 +506,15 @@ function prepareAndRegisterServiceForDocShadowMode(global, extensions, | |||
* @param {function(!./service/ampdoc-impl.AmpDoc):!Object=} opt_factory | |||
*/ | |||
function registerServiceForDoc(ampdoc, name, opt_ctor, opt_factory) { | |||
dev().assert((opt_ctor || opt_factory) && (!opt_ctor || !opt_factory), | |||
dev().assert(!opt_ctor != !opt_factory, |
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.
Shouldn't this be moved into registerServiceBuilderForDoc
?
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 already have a check in service.js for this. Do you think this check should be deleted from 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.
Yup
src/service.js
Outdated
* @typedef {{ | ||
* obj: (?Object), | ||
* promise: (?Promise|undefined), | ||
* resolve: (?function(!Object)|undefined), | ||
* ctor: (?function(new:Object, !Window)|?function(new:Object, !./service/ampdoc-impl.AmpDoc)), | ||
* factory: (?function(!Window)|?function(!./service/ampdoc-impl.AmpDoc)) |
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.
(?function(!Window):!Object|?function(!./service/ampdoc-impl.AmpDoc):Object)
Maybe (?function(!Window|!./service/ampdoc-impl.AmpDoc):!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.
First one SGTM. The second seems incorrect to me. We want either a function that takes in a window, or a function that takes in an ampdoc. That reads to me as "a function that can take either a window or an ampdoc" which isn't what we want.
src/service.js
Outdated
// pending promise we need to fulfill. | ||
const p = getServicePromiseOrNullInternal(win, id); | ||
if (opt_instantiate || p) { | ||
/** Force instantiation and resolve service promise if it exists */ |
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.
Nit: Use //
style comment here.
Still not quite sure about this PR yet. The following sequence of events doesn't quite work out 1: Service is registered, but not instantiated If this happens, the promise never resolves. The requester may not have access to the argument the constructor/factory needs to create the service, so I don't think we can just have the caller pass it in. I think the only viable solution here is to also store the context with the constructor and factory. Thoughts? |
Isn't it always |
Oops, please add a unit test for this scenario too. Another option is to store a closure that generates the service with the |
@jridgewell it's either a window or an ampdoc. @choumx I think the closure is probably the cleanest option and could allow us to clean up what we're putting on the service as we won't need distinct properties for constructor vs factory. |
Thanks for your patience on the many rounds of comments on this PR. I've addressed the issue I mentioned before and the code seems a lot cleaner for it. PTAL when you have a chance. |
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.
Thanks for taking the time to clean up the PR on this new revision.
src/service.js
Outdated
if (s.promise && s.resolve) { | ||
s.resolve(s.build()); | ||
} | ||
|
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.
Nit: Extra line.
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.
Done.
src/service.js
Outdated
} | ||
|
||
if (s.build) { | ||
// Service already registered |
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.
Warn?
Also, period at end of sentence (nit).
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.
Added the period. Not sure that warning here is a good idea. We will end up registering a service multiple times anyway since any time a service is requested, we'll need to make sure it's registered first. Registrations after the first will just noop (as they'll bail out on this line)
s.resolve = resolve; | ||
// Instantiate service immediately. | ||
if (s.build) { | ||
s.obj = s.build(); |
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 only want the object to be built once. Doesn't this build it on every invocation?
Please add a unit test for this.
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. 😄 Do we have a unit test for this case though?
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.
It's admittedly a bit confusing. The call to getServicePromiseOrnullInternal will return a promise if the service has already been built, preventing this from executing more than once. I've added a test regardless.
test/functional/test-service.js
Outdated
it('should only instantiate the service once', () => { | ||
registerServiceBuilder(window, 'b', Class); | ||
expect(count).to.equal(0); | ||
for (let i = 0; i < 10; 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.
Just two getService
calls would suffice here.
This PR creates a distinction between registering/installing a service. Installation/registration of a service now registers a constructor that is used to instantiate the service the first time the service is requested.
Installation should be idempotent; registering a service multiple times should not clear out an existing instance of the service.
Part 1 of #4986
/to @choumx @jridgewell