Skip to content

Commit

Permalink
runOutsideAngular for Universal compatibility and allow advanced conf…
Browse files Browse the repository at this point in the history
…iguration with DI (#1454)

* First.

* SSR work

* StorageBucket is a string...

* Add AngularFireModule back in

* Fix up storage and the tests

* Adding StorageBucket and DatabaseURL DI tests

* Adding specs for DI in auth + firestore

* More complete tests + fixed the storage test config

* Adding app.module back in and writing tests for app injection

* Export RealtimeDatabaseURL from database-deprecated

* Export FirebaseApp

* Working, needs wrapper

* More

* State changes and auditlog

* Wrap auth

* Reverted database-depricated a bit

* More cleanup

* Cleanup unused imports
  • Loading branch information
jamesdaniels authored and davideast committed Mar 20, 2018
1 parent 74208a9 commit e343f13
Show file tree
Hide file tree
Showing 38 changed files with 1,878 additions and 1,414 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
"@angular/core": "^5.0.0",
"@angular/platform-browser": "^5.0.0",
"@angular/platform-browser-dynamic": "^5.0.0",
"bufferutil": "^3.0.3",
"@firebase/app": "^0.1.6",
"@firebase/app-types": "^0.1.1",
"@firebase/auth": "^0.3.2",
Expand All @@ -46,12 +45,13 @@
"@firebase/messaging-types": "^0.1.1",
"@firebase/storage": "^0.1.6",
"@firebase/storage-types": "^0.1.1",
"bufferutil": "^3.0.3",
"firebase": "^4.8.2",
"rxjs": "^5.5.4",
"utf-8-validate": "^4.0.0",
"ws": "^3.3.2",
"zone.js": "^0.8.0",
"xmlhttprequest": "^1.8.0"
"xmlhttprequest": "^1.8.0",
"zone.js": "^0.8.0"
},
"devDependencies": {
"@angular/compiler-cli": "^5.0.0",
Expand Down
20 changes: 2 additions & 18 deletions src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,8 @@
import { NgModule, NgZone } from '@angular/core';
import { FirebaseApp, AngularFireModule } from 'angularfire2';
import { NgModule } from '@angular/core';
import { AngularFireAuth } from './auth';
import '@firebase/auth';

export function _getAngularFireAuth(app: FirebaseApp) {
return new AngularFireAuth(app);
}

export const AngularFireAuthProvider = {
provide: AngularFireAuth,
useFactory: _getAngularFireAuth,
deps: [ FirebaseApp ]
};

export const AUTH_PROVIDERS = [
AngularFireAuthProvider,
];

@NgModule({
imports: [ AngularFireModule ],
providers: [ AUTH_PROVIDERS ]
providers: [ AngularFireAuth ]
})
export class AngularFireAuthModule { }
56 changes: 52 additions & 4 deletions src/auth/auth.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { FirebaseApp as FBApp } from '@firebase/app-types';
import { User } from '@firebase/auth-types';
import { ReflectiveInjector, Provider } from '@angular/core';
import { Observable } from 'rxjs/Observable'
Expand All @@ -8,7 +7,7 @@ import { TestBed, inject } from '@angular/core/testing';
import { _do } from 'rxjs/operator/do';
import { take } from 'rxjs/operator/take';
import { skip } from 'rxjs/operator/skip';
import { FirebaseApp, FirebaseAppConfig, AngularFireModule } from 'angularfire2';
import { FirebaseApp, FirebaseAppConfig, AngularFireModule, FirebaseAppName } from 'angularfire2';
import { AngularFireAuth, AngularFireAuthModule } from 'angularfire2/auth';
import { COMMON_CONFIG } from './test-config';

Expand All @@ -26,7 +25,7 @@ const firebaseUser = <User> {
};

describe('AngularFireAuth', () => {
let app: FBApp;
let app: FirebaseApp;
let afAuth: AngularFireAuth;
let authSpy: jasmine.Spy;
let mockAuthState: Subject<User>;
Expand All @@ -51,7 +50,7 @@ describe('AngularFireAuth', () => {
});

afterEach(done => {
app.delete().then(done, done.fail);
afAuth.auth.app.delete().then(done, done.fail);
});

describe('Zones', () => {
Expand Down Expand Up @@ -85,6 +84,11 @@ describe('AngularFireAuth', () => {
expect(afAuth.auth).toBeDefined();
});

it('should have an initialized Firebase app', () => {
expect(afAuth.auth.app).toBeDefined();
expect(afAuth.auth.app).toEqual(app);
});

it('should emit auth updates through authState', (done: any) => {
let count = 0;

Expand Down Expand Up @@ -123,3 +127,47 @@ describe('AngularFireAuth', () => {

});

const FIREBASE_APP_NAME_TOO = (Math.random() + 1).toString(36).substring(7);

describe('AngularFireAuth with different app', () => {
let app: FirebaseApp;
let afAuth: AngularFireAuth;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [
AngularFireModule.initializeApp(COMMON_CONFIG),
AngularFireAuthModule
],
providers: [
{ provide: FirebaseAppName, useValue: FIREBASE_APP_NAME_TOO },
{ provide: FirebaseAppConfig, useValue: COMMON_CONFIG }
]
});
inject([FirebaseApp, AngularFireAuth], (app_: FirebaseApp, _afAuth: AngularFireAuth) => {
app = app_;
afAuth = _afAuth;
})();
});

afterEach(done => {
app.delete().then(done, done.fail);
});

describe('<constructor>', () => {

it('should be an AngularFireAuth type', () => {
expect(afAuth instanceof AngularFireAuth).toEqual(true);
});

it('should have an initialized Firebase app', () => {
expect(afAuth.auth.app).toBeDefined();
expect(afAuth.auth.app).toEqual(app);
});

it('should have an initialized Firebase app instance member', () => {
expect(afAuth.auth.app.name).toEqual(FIREBASE_APP_NAME_TOO);
});
});

});
45 changes: 31 additions & 14 deletions src/auth/auth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { FirebaseAuth, User } from '@firebase/auth-types';
import { Injectable, NgZone } from '@angular/core';
import { FirebaseOptions } from '@firebase/app-types';
import { Injectable, Inject, Optional, NgZone } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { observeOn } from 'rxjs/operator/observeOn';
import { FirebaseApp, ZoneScheduler } from 'angularfire2';

import { FirebaseAppConfig, FirebaseAppName, _firebaseAppFactory, FirebaseZoneScheduler } from 'angularfire2';

import 'rxjs/add/operator/switchMap';
import 'rxjs/add/observable/of';
Expand All @@ -26,22 +28,37 @@ export class AngularFireAuth {
*/
public readonly idToken: Observable<string|null>;

constructor(public app: FirebaseApp) {
this.auth = app.auth();

const authState$ = new Observable(subscriber => {
const unsubscribe = this.auth.onAuthStateChanged(subscriber);
return { unsubscribe };
constructor(
@Inject(FirebaseAppConfig) config:FirebaseOptions,
@Optional() @Inject(FirebaseAppName) name:string,
private zone: NgZone
) {
const scheduler = new FirebaseZoneScheduler(zone);
this.auth = zone.runOutsideAngular(() => {
const app = _firebaseAppFactory(config, name);
return app.auth();
});
this.authState = observeOn.call(authState$, new ZoneScheduler(Zone.current));

const idToken$ = new Observable<User|null>(subscriber => {
const unsubscribe = this.auth.onIdTokenChanged(subscriber);
return { unsubscribe };
}).switchMap(user => {
this.authState = scheduler.keepUnstableUntilFirst(
scheduler.runOutsideAngular(
new Observable(subscriber => {
const unsubscribe = this.auth.onAuthStateChanged(subscriber);
return { unsubscribe };
})
)
);

this.idToken = scheduler.keepUnstableUntilFirst(
scheduler.runOutsideAngular(
new Observable(subscriber => {
const unsubscribe = this.auth.onIdTokenChanged(subscriber);
return { unsubscribe };
})
)
).switchMap((user:User|null) => {
return user ? Observable.fromPromise(user.getIdToken()) : Observable.of(null)
});
this.idToken = observeOn.call(idToken$, new ZoneScheduler(Zone.current));

}

}
2 changes: 1 addition & 1 deletion src/core/angularfire2.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe('angularfire', () => {

inject([FirebaseApp, PlatformRef], (_app: FirebaseApp, _platform: PlatformRef) => {
app = _app;
rootRef = app.database().ref();
rootRef = app.database!().ref();
questionsRef = rootRef.child('questions');
listOfQuestionsRef = rootRef.child('list-of-questions');
defaultPlatform = _platform;
Expand Down
77 changes: 33 additions & 44 deletions src/core/angularfire2.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,42 @@
import { FirebaseAppConfigToken, FirebaseApp, _firebaseAppFactory } from './firebase.app.module';
import { Injectable, InjectionToken, NgModule } from '@angular/core';
import { InjectionToken, NgZone } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { Scheduler } from 'rxjs/Scheduler';
import { Observable } from 'rxjs/Observable';
import { queue } from 'rxjs/scheduler/queue';

export interface FirebaseAppConfig {
apiKey?: string;
authDomain?: string;
databaseURL?: string;
storageBucket?: string;
messagingSenderId?: string;
projectId?: string;
}
import firebase from '@firebase/app';
import { FirebaseApp, FirebaseOptions } from '@firebase/app-types';

const FirebaseAppName = new InjectionToken<string>('FirebaseAppName');
import 'zone.js';
import 'rxjs/add/operator/first';
import { Subscriber } from 'rxjs/Subscriber';
import { observeOn } from 'rxjs/operator/observeOn';

export const FirebaseAppProvider = {
provide: FirebaseApp,
useFactory: _firebaseAppFactory,
deps: [ FirebaseAppConfigToken, FirebaseAppName ]
};
export const FirebaseAppName = new InjectionToken<string>('angularfire2.appName');
export const FirebaseAppConfig = new InjectionToken<FirebaseOptions>('angularfire2.config');

@NgModule({
providers: [ FirebaseAppProvider ],
})
export class AngularFireModule {
static initializeApp(config: FirebaseAppConfig, appName?: string) {
return {
ngModule: AngularFireModule,
providers: [
{ provide: FirebaseAppConfigToken, useValue: config },
{ provide: FirebaseAppName, useValue: appName }
]
}
}
}

/**
* TODO: remove this scheduler once Rx has a more robust story for working
* with zones.
*/
export class ZoneScheduler {

// TODO: Correctly add ambient zone typings instead of using any.
constructor(public zone: any) {}
// Put in database.ts when we drop database-depreciated
export const RealtimeDatabaseURL = new InjectionToken<string>('angularfire2.realtimeDatabaseURL');

export class FirebaseZoneScheduler {
constructor(public zone: NgZone) {}
schedule(...args: any[]): Subscription {
return <Subscription>this.zone.run(() => queue.schedule.apply(queue, args));
return <Subscription>this.zone.runGuarded(function() { return queue.schedule.apply(queue, args)});
}
}

export { FirebaseApp, FirebaseAppName, FirebaseAppConfigToken };
// TODO this is a hack, clean it up
keepUnstableUntilFirst<T>(obs$: Observable<T>) {
return new Observable<T>(subscriber => {
const noop = () => {};
const task = Zone.current.scheduleMacroTask('firebaseZoneBlock', noop, {}, noop, noop);
obs$.first().subscribe(() => this.zone.runOutsideAngular(() => task.invoke()));
return obs$.subscribe(subscriber);
});
}
runOutsideAngular<T>(obs$: Observable<T>): Observable<T> {
const outsideAngular = new Observable<T>(subscriber => {
return this.zone.runOutsideAngular(() => {
return obs$.subscribe(subscriber);
});
});
return observeOn.call(outsideAngular, this);
}
}
67 changes: 38 additions & 29 deletions src/core/firebase.app.module.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,49 @@
import { InjectionToken, } from '@angular/core';
import { FirebaseAppConfig } from './';
import firebase from '@firebase/app';
import { InjectionToken, NgZone, NgModule } from '@angular/core';

import { FirebaseAppConfig, FirebaseAppName } from './angularfire2';

import { FirebaseApp as FBApp } from '@firebase/app-types';
import firebase from '@firebase/app';
import { FirebaseApp as _FirebaseApp, FirebaseOptions } from '@firebase/app-types';
import { FirebaseAuth } from '@firebase/auth-types';
import { FirebaseDatabase } from '@firebase/database-types';
import { FirebaseMessaging } from '@firebase/messaging-types';
import { FirebaseStorage } from '@firebase/storage-types';
import { FirebaseFirestore } from '@firebase/firestore-types';

export const FirebaseAppConfigToken = new InjectionToken<FirebaseAppConfig>('FirebaseAppConfigToken');
export class FirebaseApp implements _FirebaseApp {
name: string;
options: {};
auth: () => FirebaseAuth;
database: (databaseURL?: string) => FirebaseDatabase;
messaging: () => FirebaseMessaging;
storage: (storageBucket?: string) => FirebaseStorage;
delete: () => Promise<void>;
firestore: () => FirebaseFirestore;
}

export class FirebaseApp implements FBApp {
name: string;
options: {};
auth: () => FirebaseAuth;
database: () => FirebaseDatabase;
messaging: () => FirebaseMessaging;
storage: () => FirebaseStorage;
delete: () => Promise<any>;
firestore: () => FirebaseFirestore;
export function _firebaseAppFactory(config: FirebaseOptions, name?: string): FirebaseApp {
const appName = name || '[DEFAULT]';
const existingApp = firebase.apps.filter(app => app.name == appName)[0] as FirebaseApp;
return existingApp || firebase.initializeApp(config, appName) as FirebaseApp;
}

export function _firebaseAppFactory(config: FirebaseAppConfig, appName?: string): FirebaseApp {
try {
if (appName) {
return firebase.initializeApp(config, appName) as FirebaseApp;
} else {
return firebase.initializeApp(config) as FirebaseApp;
const FirebaseAppProvider = {
provide: FirebaseApp,
useFactory: _firebaseAppFactory,
deps: [ FirebaseAppConfig, FirebaseAppName ]
};

@NgModule({
providers: [ FirebaseAppProvider ],
})
export class AngularFireModule {
static initializeApp(config: FirebaseOptions, appName?: string) {
return {
ngModule: AngularFireModule,
providers: [
{ provide: FirebaseAppConfig, useValue: config },
{ provide: FirebaseAppName, useValue: appName }
]
}
}
}
catch (e) {
if (e.code === "app/duplicate-app") {
return firebase.app(e.name) as FirebaseApp;
}

return firebase.app(null!) as FirebaseApp;
}
}
}
3 changes: 2 additions & 1 deletion src/core/public_api.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './angularfire2';
export * from './angularfire2';
export * from './firebase.app.module';
Loading

0 comments on commit e343f13

Please sign in to comment.