Skip to content

Commit e343f13

Browse files
jamesdanielsdavideast
authored andcommitted
runOutsideAngular for Universal compatibility and allow advanced configuration 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
1 parent 74208a9 commit e343f13

38 files changed

+1878
-1414
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
"@angular/core": "^5.0.0",
3535
"@angular/platform-browser": "^5.0.0",
3636
"@angular/platform-browser-dynamic": "^5.0.0",
37-
"bufferutil": "^3.0.3",
3837
"@firebase/app": "^0.1.6",
3938
"@firebase/app-types": "^0.1.1",
4039
"@firebase/auth": "^0.3.2",
@@ -46,12 +45,13 @@
4645
"@firebase/messaging-types": "^0.1.1",
4746
"@firebase/storage": "^0.1.6",
4847
"@firebase/storage-types": "^0.1.1",
48+
"bufferutil": "^3.0.3",
4949
"firebase": "^4.8.2",
5050
"rxjs": "^5.5.4",
5151
"utf-8-validate": "^4.0.0",
5252
"ws": "^3.3.2",
53-
"zone.js": "^0.8.0",
54-
"xmlhttprequest": "^1.8.0"
53+
"xmlhttprequest": "^1.8.0",
54+
"zone.js": "^0.8.0"
5555
},
5656
"devDependencies": {
5757
"@angular/compiler-cli": "^5.0.0",

src/auth/auth.module.ts

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,8 @@
1-
import { NgModule, NgZone } from '@angular/core';
2-
import { FirebaseApp, AngularFireModule } from 'angularfire2';
1+
import { NgModule } from '@angular/core';
32
import { AngularFireAuth } from './auth';
43
import '@firebase/auth';
54

6-
export function _getAngularFireAuth(app: FirebaseApp) {
7-
return new AngularFireAuth(app);
8-
}
9-
10-
export const AngularFireAuthProvider = {
11-
provide: AngularFireAuth,
12-
useFactory: _getAngularFireAuth,
13-
deps: [ FirebaseApp ]
14-
};
15-
16-
export const AUTH_PROVIDERS = [
17-
AngularFireAuthProvider,
18-
];
19-
205
@NgModule({
21-
imports: [ AngularFireModule ],
22-
providers: [ AUTH_PROVIDERS ]
6+
providers: [ AngularFireAuth ]
237
})
248
export class AngularFireAuthModule { }

src/auth/auth.spec.ts

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { FirebaseApp as FBApp } from '@firebase/app-types';
21
import { User } from '@firebase/auth-types';
32
import { ReflectiveInjector, Provider } from '@angular/core';
43
import { Observable } from 'rxjs/Observable'
@@ -8,7 +7,7 @@ import { TestBed, inject } from '@angular/core/testing';
87
import { _do } from 'rxjs/operator/do';
98
import { take } from 'rxjs/operator/take';
109
import { skip } from 'rxjs/operator/skip';
11-
import { FirebaseApp, FirebaseAppConfig, AngularFireModule } from 'angularfire2';
10+
import { FirebaseApp, FirebaseAppConfig, AngularFireModule, FirebaseAppName } from 'angularfire2';
1211
import { AngularFireAuth, AngularFireAuthModule } from 'angularfire2/auth';
1312
import { COMMON_CONFIG } from './test-config';
1413

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

2827
describe('AngularFireAuth', () => {
29-
let app: FBApp;
28+
let app: FirebaseApp;
3029
let afAuth: AngularFireAuth;
3130
let authSpy: jasmine.Spy;
3231
let mockAuthState: Subject<User>;
@@ -51,7 +50,7 @@ describe('AngularFireAuth', () => {
5150
});
5251

5352
afterEach(done => {
54-
app.delete().then(done, done.fail);
53+
afAuth.auth.app.delete().then(done, done.fail);
5554
});
5655

5756
describe('Zones', () => {
@@ -85,6 +84,11 @@ describe('AngularFireAuth', () => {
8584
expect(afAuth.auth).toBeDefined();
8685
});
8786

87+
it('should have an initialized Firebase app', () => {
88+
expect(afAuth.auth.app).toBeDefined();
89+
expect(afAuth.auth.app).toEqual(app);
90+
});
91+
8892
it('should emit auth updates through authState', (done: any) => {
8993
let count = 0;
9094

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

124128
});
125129

130+
const FIREBASE_APP_NAME_TOO = (Math.random() + 1).toString(36).substring(7);
131+
132+
describe('AngularFireAuth with different app', () => {
133+
let app: FirebaseApp;
134+
let afAuth: AngularFireAuth;
135+
136+
beforeEach(() => {
137+
TestBed.configureTestingModule({
138+
imports: [
139+
AngularFireModule.initializeApp(COMMON_CONFIG),
140+
AngularFireAuthModule
141+
],
142+
providers: [
143+
{ provide: FirebaseAppName, useValue: FIREBASE_APP_NAME_TOO },
144+
{ provide: FirebaseAppConfig, useValue: COMMON_CONFIG }
145+
]
146+
});
147+
inject([FirebaseApp, AngularFireAuth], (app_: FirebaseApp, _afAuth: AngularFireAuth) => {
148+
app = app_;
149+
afAuth = _afAuth;
150+
})();
151+
});
152+
153+
afterEach(done => {
154+
app.delete().then(done, done.fail);
155+
});
156+
157+
describe('<constructor>', () => {
158+
159+
it('should be an AngularFireAuth type', () => {
160+
expect(afAuth instanceof AngularFireAuth).toEqual(true);
161+
});
162+
163+
it('should have an initialized Firebase app', () => {
164+
expect(afAuth.auth.app).toBeDefined();
165+
expect(afAuth.auth.app).toEqual(app);
166+
});
167+
168+
it('should have an initialized Firebase app instance member', () => {
169+
expect(afAuth.auth.app.name).toEqual(FIREBASE_APP_NAME_TOO);
170+
});
171+
});
172+
173+
});

src/auth/auth.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { FirebaseAuth, User } from '@firebase/auth-types';
2-
import { Injectable, NgZone } from '@angular/core';
2+
import { FirebaseOptions } from '@firebase/app-types';
3+
import { Injectable, Inject, Optional, NgZone } from '@angular/core';
34
import { Observable } from 'rxjs/Observable';
45
import { observeOn } from 'rxjs/operator/observeOn';
5-
import { FirebaseApp, ZoneScheduler } from 'angularfire2';
6+
7+
import { FirebaseAppConfig, FirebaseAppName, _firebaseAppFactory, FirebaseZoneScheduler } from 'angularfire2';
68

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

29-
constructor(public app: FirebaseApp) {
30-
this.auth = app.auth();
31-
32-
const authState$ = new Observable(subscriber => {
33-
const unsubscribe = this.auth.onAuthStateChanged(subscriber);
34-
return { unsubscribe };
31+
constructor(
32+
@Inject(FirebaseAppConfig) config:FirebaseOptions,
33+
@Optional() @Inject(FirebaseAppName) name:string,
34+
private zone: NgZone
35+
) {
36+
const scheduler = new FirebaseZoneScheduler(zone);
37+
this.auth = zone.runOutsideAngular(() => {
38+
const app = _firebaseAppFactory(config, name);
39+
return app.auth();
3540
});
36-
this.authState = observeOn.call(authState$, new ZoneScheduler(Zone.current));
3741

38-
const idToken$ = new Observable<User|null>(subscriber => {
39-
const unsubscribe = this.auth.onIdTokenChanged(subscriber);
40-
return { unsubscribe };
41-
}).switchMap(user => {
42+
this.authState = scheduler.keepUnstableUntilFirst(
43+
scheduler.runOutsideAngular(
44+
new Observable(subscriber => {
45+
const unsubscribe = this.auth.onAuthStateChanged(subscriber);
46+
return { unsubscribe };
47+
})
48+
)
49+
);
50+
51+
this.idToken = scheduler.keepUnstableUntilFirst(
52+
scheduler.runOutsideAngular(
53+
new Observable(subscriber => {
54+
const unsubscribe = this.auth.onIdTokenChanged(subscriber);
55+
return { unsubscribe };
56+
})
57+
)
58+
).switchMap((user:User|null) => {
4259
return user ? Observable.fromPromise(user.getIdToken()) : Observable.of(null)
4360
});
44-
this.idToken = observeOn.call(idToken$, new ZoneScheduler(Zone.current));
61+
4562
}
4663

4764
}

src/core/angularfire2.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ describe('angularfire', () => {
2525

2626
inject([FirebaseApp, PlatformRef], (_app: FirebaseApp, _platform: PlatformRef) => {
2727
app = _app;
28-
rootRef = app.database().ref();
28+
rootRef = app.database!().ref();
2929
questionsRef = rootRef.child('questions');
3030
listOfQuestionsRef = rootRef.child('list-of-questions');
3131
defaultPlatform = _platform;

src/core/angularfire2.ts

Lines changed: 33 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,42 @@
1-
import { FirebaseAppConfigToken, FirebaseApp, _firebaseAppFactory } from './firebase.app.module';
2-
import { Injectable, InjectionToken, NgModule } from '@angular/core';
1+
import { InjectionToken, NgZone } from '@angular/core';
32
import { Subscription } from 'rxjs/Subscription';
4-
import { Scheduler } from 'rxjs/Scheduler';
3+
import { Observable } from 'rxjs/Observable';
54
import { queue } from 'rxjs/scheduler/queue';
65

7-
export interface FirebaseAppConfig {
8-
apiKey?: string;
9-
authDomain?: string;
10-
databaseURL?: string;
11-
storageBucket?: string;
12-
messagingSenderId?: string;
13-
projectId?: string;
14-
}
6+
import firebase from '@firebase/app';
7+
import { FirebaseApp, FirebaseOptions } from '@firebase/app-types';
158

16-
const FirebaseAppName = new InjectionToken<string>('FirebaseAppName');
9+
import 'zone.js';
10+
import 'rxjs/add/operator/first';
11+
import { Subscriber } from 'rxjs/Subscriber';
12+
import { observeOn } from 'rxjs/operator/observeOn';
1713

18-
export const FirebaseAppProvider = {
19-
provide: FirebaseApp,
20-
useFactory: _firebaseAppFactory,
21-
deps: [ FirebaseAppConfigToken, FirebaseAppName ]
22-
};
14+
export const FirebaseAppName = new InjectionToken<string>('angularfire2.appName');
15+
export const FirebaseAppConfig = new InjectionToken<FirebaseOptions>('angularfire2.config');
2316

24-
@NgModule({
25-
providers: [ FirebaseAppProvider ],
26-
})
27-
export class AngularFireModule {
28-
static initializeApp(config: FirebaseAppConfig, appName?: string) {
29-
return {
30-
ngModule: AngularFireModule,
31-
providers: [
32-
{ provide: FirebaseAppConfigToken, useValue: config },
33-
{ provide: FirebaseAppName, useValue: appName }
34-
]
35-
}
36-
}
37-
}
38-
39-
/**
40-
* TODO: remove this scheduler once Rx has a more robust story for working
41-
* with zones.
42-
*/
43-
export class ZoneScheduler {
44-
45-
// TODO: Correctly add ambient zone typings instead of using any.
46-
constructor(public zone: any) {}
17+
// Put in database.ts when we drop database-depreciated
18+
export const RealtimeDatabaseURL = new InjectionToken<string>('angularfire2.realtimeDatabaseURL');
4719

20+
export class FirebaseZoneScheduler {
21+
constructor(public zone: NgZone) {}
4822
schedule(...args: any[]): Subscription {
49-
return <Subscription>this.zone.run(() => queue.schedule.apply(queue, args));
23+
return <Subscription>this.zone.runGuarded(function() { return queue.schedule.apply(queue, args)});
5024
}
51-
}
52-
53-
export { FirebaseApp, FirebaseAppName, FirebaseAppConfigToken };
25+
// TODO this is a hack, clean it up
26+
keepUnstableUntilFirst<T>(obs$: Observable<T>) {
27+
return new Observable<T>(subscriber => {
28+
const noop = () => {};
29+
const task = Zone.current.scheduleMacroTask('firebaseZoneBlock', noop, {}, noop, noop);
30+
obs$.first().subscribe(() => this.zone.runOutsideAngular(() => task.invoke()));
31+
return obs$.subscribe(subscriber);
32+
});
33+
}
34+
runOutsideAngular<T>(obs$: Observable<T>): Observable<T> {
35+
const outsideAngular = new Observable<T>(subscriber => {
36+
return this.zone.runOutsideAngular(() => {
37+
return obs$.subscribe(subscriber);
38+
});
39+
});
40+
return observeOn.call(outsideAngular, this);
41+
}
42+
}

src/core/firebase.app.module.ts

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,49 @@
1-
import { InjectionToken, } from '@angular/core';
2-
import { FirebaseAppConfig } from './';
3-
import firebase from '@firebase/app';
1+
import { InjectionToken, NgZone, NgModule } from '@angular/core';
2+
3+
import { FirebaseAppConfig, FirebaseAppName } from './angularfire2';
44

5-
import { FirebaseApp as FBApp } from '@firebase/app-types';
5+
import firebase from '@firebase/app';
6+
import { FirebaseApp as _FirebaseApp, FirebaseOptions } from '@firebase/app-types';
67
import { FirebaseAuth } from '@firebase/auth-types';
78
import { FirebaseDatabase } from '@firebase/database-types';
89
import { FirebaseMessaging } from '@firebase/messaging-types';
910
import { FirebaseStorage } from '@firebase/storage-types';
1011
import { FirebaseFirestore } from '@firebase/firestore-types';
1112

12-
export const FirebaseAppConfigToken = new InjectionToken<FirebaseAppConfig>('FirebaseAppConfigToken');
13+
export class FirebaseApp implements _FirebaseApp {
14+
name: string;
15+
options: {};
16+
auth: () => FirebaseAuth;
17+
database: (databaseURL?: string) => FirebaseDatabase;
18+
messaging: () => FirebaseMessaging;
19+
storage: (storageBucket?: string) => FirebaseStorage;
20+
delete: () => Promise<void>;
21+
firestore: () => FirebaseFirestore;
22+
}
1323

14-
export class FirebaseApp implements FBApp {
15-
name: string;
16-
options: {};
17-
auth: () => FirebaseAuth;
18-
database: () => FirebaseDatabase;
19-
messaging: () => FirebaseMessaging;
20-
storage: () => FirebaseStorage;
21-
delete: () => Promise<any>;
22-
firestore: () => FirebaseFirestore;
24+
export function _firebaseAppFactory(config: FirebaseOptions, name?: string): FirebaseApp {
25+
const appName = name || '[DEFAULT]';
26+
const existingApp = firebase.apps.filter(app => app.name == appName)[0] as FirebaseApp;
27+
return existingApp || firebase.initializeApp(config, appName) as FirebaseApp;
2328
}
2429

25-
export function _firebaseAppFactory(config: FirebaseAppConfig, appName?: string): FirebaseApp {
26-
try {
27-
if (appName) {
28-
return firebase.initializeApp(config, appName) as FirebaseApp;
29-
} else {
30-
return firebase.initializeApp(config) as FirebaseApp;
30+
const FirebaseAppProvider = {
31+
provide: FirebaseApp,
32+
useFactory: _firebaseAppFactory,
33+
deps: [ FirebaseAppConfig, FirebaseAppName ]
34+
};
35+
36+
@NgModule({
37+
providers: [ FirebaseAppProvider ],
38+
})
39+
export class AngularFireModule {
40+
static initializeApp(config: FirebaseOptions, appName?: string) {
41+
return {
42+
ngModule: AngularFireModule,
43+
providers: [
44+
{ provide: FirebaseAppConfig, useValue: config },
45+
{ provide: FirebaseAppName, useValue: appName }
46+
]
47+
}
3148
}
32-
}
33-
catch (e) {
34-
if (e.code === "app/duplicate-app") {
35-
return firebase.app(e.name) as FirebaseApp;
36-
}
37-
38-
return firebase.app(null!) as FirebaseApp;
39-
}
40-
}
49+
}

src/core/public_api.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
export * from './angularfire2';
1+
export * from './angularfire2';
2+
export * from './firebase.app.module';

0 commit comments

Comments
 (0)