Skip to content
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

Merged
merged 18 commits into from
Mar 9, 2017

Conversation

kmh287
Copy link
Contributor

@kmh287 kmh287 commented Feb 28, 2017

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

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) {
Copy link
Contributor

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?

Copy link
Contributor Author

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
Copy link
Contributor

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?

Copy link
Contributor Author

@kmh287 kmh287 Mar 1, 2017

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

@kmh287 kmh287 requested a review from jridgewell March 3, 2017 16:59
Copy link

@dreamofabear dreamofabear left a 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,

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.

Copy link
Contributor Author

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,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

|| undefined is unnecessary.

Copy link
Contributor Author

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}`);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: +2 spaces

Copy link
Contributor Author

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) {

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?

Copy link
Contributor Author

@kmh287 kmh287 Mar 3, 2017

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.

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.

Copy link
Contributor Author

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) {

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.

Copy link
Contributor Author

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);

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?

Copy link
Contributor Author

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?

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?

Copy link
Contributor Author

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.

@kmh287
Copy link
Contributor Author

kmh287 commented Mar 3, 2017

@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,
Copy link
Contributor

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?

Copy link
Contributor Author

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?

Copy link
Contributor

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))
Copy link
Contributor

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)?

Copy link
Contributor Author

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 */

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.

@kmh287
Copy link
Contributor Author

kmh287 commented Mar 6, 2017

@choumx @jridgewell

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
2: Service is requested by promise

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?

@jridgewell
Copy link
Contributor

The requester may not have access to the argument the constructor/factory needs to create the service

Isn't it always AMP?

@dreamofabear
Copy link

Oops, please add a unit test for this scenario too. Another option is to store a closure that generates the service with the ctor or factory.

@kmh287
Copy link
Contributor Author

kmh287 commented Mar 7, 2017

@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.

@kmh287
Copy link
Contributor Author

kmh287 commented Mar 7, 2017

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.

Copy link

@dreamofabear dreamofabear left a 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());
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Extra line.

Copy link
Contributor Author

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

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).

Copy link
Contributor Author

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();

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.

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?

Copy link
Contributor Author

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.

it('should only instantiate the service once', () => {
registerServiceBuilder(window, 'b', Class);
expect(count).to.equal(0);
for (let i = 0; i < 10; i++) {

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants