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

Allow for lazy registration of app components #15

Merged
merged 9 commits into from May 25, 2017
Merged

Conversation

jshcrowthe
Copy link
Contributor

@jshcrowthe jshcrowthe commented May 18, 2017

Fixes #14

Live demo available here:
http://jsbin.com/zozuqeh/edit?js,console

@mbleigh
Copy link

mbleigh commented May 18, 2017

Well that's a surprisingly small change. I think the concern previously was cross-product hooks, particularly with Auth. Have you tried this out with loading the Database first, then Auth later?

So 1) load DB, read some data that doesn't require auth 2) load auth lazily, load some data from the DB that does require auth.

@jshcrowthe
Copy link
Contributor Author

jshcrowthe commented May 18, 2017

@mbleigh So the cross-product hooks that you mentioned are all taken care of. However I did update my JSBin to demo your use case and discovered that the stub methods that FirebaseApp creates for the auth, needs to forward the listeners created on to the auth module if it is lazily instantiated. Should hopefully have that working in just a bit.

AFAICT none of the other products have this issue though as they all call into getToken at the time of the call whereas database needs to register token listeners.

@justweb1
Copy link

justweb1 commented May 21, 2017

@jshcrowthe I was trying to figure out a way to auto inititializeApp when hosted on Firebase. I was trying this through Polymerfire but the issue @mbleigh brought up was lazy loading the elements. Would it be appropriate to add a test to your PR to see if '/__/firebase/init.js' is present and then import it and run initializeApp automatically?

Copy link

@mbleigh mbleigh left a comment

Choose a reason for hiding this comment

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

I think my main question/concern at this point is that there is now auth-specific code hanging out in FirebaseApp. Is there any way to expose hooks in FirebaseApp that are instead consumed by Firebase Auth in such a way that the same behavior happens Right now this feels like an awesome proof-of-concept but not necessarily a stable foundation.

@jshcrowthe
Copy link
Contributor Author

jshcrowthe commented May 22, 2017

The reason why I followed this approach is largely based on how modules are currently required to register with Firebase App.

We are currently stubbing some auth-specific code that is required by certain modules (i.e. storage, database) in the event that auth does not exist (see https://github.com/firebase/firebase-js-sdk/blob/master/src/app/firebase_app.ts#L433-L448). This implies a dependency of each of these components upon those auth methods. As our source is currently spread across module implementations (some rely on imports and our webpack compilation, others rely on the firebase and firebase.INTERNAL namespaces), we have some workarounds in place to help bridge this gap (namely the firebase.INTERNAL utilities).

Once we have access to the Auth source in this repo, a refactoring of this code (i.e. removal of this code) and the associated database/storage code, should happen that would extract this specific module into a standalone piece. This piece would stub the API and provide each component (in this case auth) the ability to control how this functionality is "upgraded." That said, with how we currently distribute to CDNs, the "stubbing" module would be included with firebase-app.js as this code is shared across webpack entry points.

I will add a comment to explain the need for this and a "todo" to remove it once able?

private services_: {[name: string]:
{[instance: string]: FirebaseService}} = {};
public INTERNAL: FirebaseAppInternals;
private _registeredServices: {
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't seem to be used. Also, you seem to be prefixing private variables and methods with an underscore. This is not the convention here. I think we should stick to one format. In general the typescript convention is not to use leading or lagging underscores for private variables/methods.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch I'll remove this. It was an artifact of an old implementation.

*/
private getService(name: string, instanceString?: string): FirebaseService
Copy link
Contributor

Choose a reason for hiding this comment

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

What happened to this? By not tracking this, I think you will return the same default instance for storage. I think you could have multiple instances of storage with different buckets.

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 see what you mean! I'm surprised we don't have a test for this. I'll revert the majority of this change (it didn't seem used so it was a clean up thing) and add a test to make sure we don't break that going forward! I will need to keep the removal of the private keyword as we need to call this outside of this classes implementation details.

if (appHook) {
appHooks[name] = appHook;
getApps().forEach(app => {
appHook('create', app);
Copy link
Contributor

Choose a reason for hiding this comment

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

This will trigger every time a service is registered to the same app. Even when it was previously already triggered. Not sure what the implications are for other services listening to this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The two functions that currently will trigger the create hook for services are initializeApp and registerService. Though they are similar they are slightly different and actual have no overlap in terms of when appHooks are fired.

  • initializeApp attempts to run appHooks for each factory that exists at the time of app initialization.
  • registerService runs only the new appHook (if applicable) on all existing instances of FirebaseApp it then also registers itself as one of the available appHooks so that future initializeApp calls will trigger the hook.

The create hook will be run once, and only once, per instance of Firebase App. Because one can't register multiple services of the same name, attempting to register multiple appHooks under the same name, and trigger a duplicate function call, should fail with an error long before the call is ever attempted.

Copy link

@mbleigh mbleigh left a comment

Choose a reason for hiding this comment

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

I AM EXCITE!

However, folks more familiar with the SDK should approve before this gets merged. Let's make sure we do a minor version bump when this gets released.

There was an issue where we weren't properly limiting mulitple service instances. Added a test and
refactored the code to support that use case
@jshcrowthe
Copy link
Contributor Author

@bojeil-google I have addressed some of your concerns and added some tests to verify that functionality stays functioning (turns out there was an alternate way to do it that I brought into compliance with the API).

@jshcrowthe
Copy link
Contributor Author

@justweb1 I don't know if this PR is the right place to add that type of functionality. That said, I think if we were to do something explicit (i.e. something like allowing people to call firebase.initializeApp w/o any args, or a separate function altogether) for this that'd work.

However I can also see this type of functionality being a nice thing for an abstraction layer like Polymerfire, angularfire, etc.

Thoughts?

@justweb1
Copy link

The thought was originally for it to be built into Polymerfire and I had even started work on it. After talking with @mbleigh, it seems to make more sense to run it directly in the sdk. One of the main issues I was having was due to the fact I needed lazy loading. Then, when it boils down to it placing a check and calling the init.js directly from the sdk is a much better option and will require way less code. This will also simplify the initialization and improve user experience.

@jshcrowthe
Copy link
Contributor Author

@justweb1 let's open a separate issue for this and continue discussion there.

* otherwise, return a proxied reference to the same service
*/
factories[name] = allowMultipleInstances ? createService :
(...args) => createService.apply(this, args);
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like this ternary statement isn't really doing anything? Before, the if statement was always sending DEFAULT_ENTRY_NAME instead of the passed instanceString if !allowMultipleInstances, but since you handle the multiple instance name thing further down now (inside the function assigned to FirebaseAppImpl.prototype[name]), you should just be able to do

factories[name] = createService;

and have it work, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great point, I think I had meant to do this, but never actually did it. Fixed!

Copy link
Contributor

@sphippen sphippen left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Member

@davideast davideast left a comment

Choose a reason for hiding this comment

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

If you make @sphippen happy, then I'm happy. Let's get this in 💥

@jshcrowthe jshcrowthe self-assigned this May 24, 2017
FirebaseAppImpl.prototype[name] = function(...args) {
const serviceFxn = this._getService.bind(this, name);
return serviceFxn.apply(this, allowMultipleInstances ? args : []);
}
Copy link
Contributor

@bojeil-google bojeil-google May 25, 2017

Choose a reason for hiding this comment

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

It's late, maybe I'm misreading:
So this will patch in the service factory: app.storage(instanceString);
Will this pass instanceString to createService(app, extendApp, instanceString)?

This statement, as quoted from above, doesn't pass the instance string.

let service = this.firebase_.INTERNAL.factories[name](this, this.extendApp.bind(this));

If you update the test below where I commented to confirm this, then you are good to go.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great catch on this. I have refactored to ensure that we are passing through the instanceIdentifier I must have missed it in adding code back.

In addition I have added an additional assertion to two of the tests below to ensure that we don't break this going forward.

const service2 = (firebase.app() as any).multiInstance(serviceIdentifier);
assert.strictEqual(service2, (firebase.app() as any).multiInstance(serviceIdentifier));

// Ensure that the two services **are not equal**
Copy link
Contributor

@bojeil-google bojeil-google May 25, 2017

Choose a reason for hiding this comment

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

This doesn't check that the two difference services are getting their identifier passed to the underlying service constructor.
For Storage, this is:

function factory(app: FirebaseApp, unused: any, opt_url?: string): Service {
  return new Service(app, new XhrIoPool(), opt_url);
}

opt_url needs to be passed through.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Gotcha, thanks!

Fixed an issue where instance identifiers weren't being passed to their creation factories. Added
some tests to validate that we don't accidentally remove that in the future.
@jshcrowthe
Copy link
Contributor Author

@bojeil-google I have responded to your last two comments. Thanks sir!

We should be good to go. I'll get this merged.

@jshcrowthe jshcrowthe merged commit bd24f71 into master May 25, 2017
@jshcrowthe jshcrowthe deleted the lazy-register branch May 25, 2017 16:12
@bojeil-google
Copy link
Contributor

Looks good. Thanks!

jshcrowthe added a commit that referenced this pull request May 25, 2017
* WIP: add lazy module instantiation

* feat(app): allow for lazy loading of services

* test(app): add integration test for webpack/browserify for lazy instantiation

* fix(app): fix issue where auth listeners weren't being patched properly

* docs(app): adding doc explaining Firebase Auth specific code in firebase_app.ts

* fix(app): revert code refactor to _getService function

* test(app): add tests and fix issue with multiple instances of a service

There was an issue where we weren't properly limiting mulitple service instances. Added a test and
refactored the code to support that use case

* refactor(app): remove unneeded ternary

* fix(app): fix issue where instanceIdentifier wasn't being passed

Fixed an issue where instance identifiers weren't being passed to their creation factories. Added
some tests to validate that we don't accidentally remove that in the future.
@firebase firebase locked and limited conversation to collaborators Oct 27, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Cannot lazily load firebase components
7 participants