Using our subject/cache to fix the init race I introduced (#218)

* Using our subject/cache to fix the init race I introduced with the refactor

* Naming

* Await init on non-contextual app, if one is passed

* void | Promise<any> better describes the contract

* Global cache and test
This commit is contained in:
James Daniels
2020-02-25 15:47:33 -08:00
committed by GitHub
parent 32c2bb3c40
commit ce716feab1
6 changed files with 132 additions and 59 deletions

View File

@@ -6,7 +6,7 @@ export * from './sdk';
type FirebaseAppContextValue = firebase.app.App;
// INVESTIGATE I don't like magic strings, can we have export this in js-sdk?
export const DEFAULT_APP_NAME = '[DEFAULT]';
const DEFAULT_APP_NAME = '[DEFAULT]';
const FirebaseAppContext = React.createContext<
FirebaseAppContextValue | undefined

View File

@@ -1,6 +1,7 @@
import { DEFAULT_APP_NAME } from '../index';
import { useFirebaseApp } from '.';
import * as firebase from 'firebase/app';
import { Observable } from 'rxjs';
import { preloadObservable } from '../useObservable';
type ComponentName =
| 'analytics'
@@ -62,17 +63,19 @@ function proxyComponent(
componentName: ComponentName
): FirebaseNamespaceComponent {
let contextualApp: App | undefined;
const useComponent = () => {
const useComponent = (app?: App) => {
contextualApp = useFirebaseApp();
if (!firebase[componentName]) {
throw importSDK(componentName);
const sdkSubject = preload(componentName, app || contextualApp);
if (!sdkSubject.hasValue) {
throw sdkSubject.firstEmission;
}
sdkSubject.value; // get value to throw if there's an error
return firebase[componentName];
};
return new Proxy(useComponent, {
get: (target, p) => target()[p],
apply: (target, _this, args) => {
const component = target().bind(_this);
const component = target(args[0]).bind(_this);
// If they don't pass an app, assume the app in context rather than [DEFAULT]
if (!args[0]) {
args[0] = contextualApp;
@@ -102,94 +105,100 @@ export const performance = usePerformance;
export const remoteConfig = useRemoteConfig;
export const storage = useStorage;
function preload(
function preloadFactory(
componentName: 'auth'
): (
firebaseApp?: App,
settingsCallback?: (instanceFactory: App['auth']) => any
settingsCallback?: (instanceFactory: App['auth']) => void | Promise<any>
) => Promise<App['auth']>;
function preload(
function preloadFactory(
componentName: 'analytics'
): (
firebaseApp?: App,
settingsCallback?: (instanceFactory: App['analytics']) => any
settingsCallback?: (instanceFactory: App['analytics']) => void | Promise<any>
) => Promise<App['analytics']>;
function preload(
function preloadFactory(
componentName: 'database'
): (
firebaseApp?: App,
settingsCallback?: (instanceFactory: App['database']) => any
settingsCallback?: (instanceFactory: App['database']) => void | Promise<any>
) => Promise<App['database']>;
function preload(
function preloadFactory(
componentName: 'firestore'
): (
firebaseApp?: App,
settingsCallback?: (instanceFactory: App['firestore']) => any
settingsCallback?: (instanceFactory: App['firestore']) => void | Promise<any>
) => Promise<App['firestore']>;
function preload(
function preloadFactory(
componentName: 'functions'
): (
firebaseApp?: App,
settingsCallback?: (instanceFactory: App['functions']) => any
settingsCallback?: (instanceFactory: App['functions']) => void | Promise<any>
) => Promise<App['functions']>;
function preload(
function preloadFactory(
componentName: 'messaging'
): (
firebaseApp?: App,
settingsCallback?: (instanceFactory: App['messaging']) => any
settingsCallback?: (instanceFactory: App['messaging']) => void | Promise<any>
) => Promise<App['messaging']>;
function preload(
function preloadFactory(
componentName: 'performance'
): (
firebaseApp?: App,
settingsCallback?: (instanceFactory: App['performance']) => any
settingsCallback?: (instanceFactory: App['performance']) => void | Promise<any>
) => Promise<App['performance']>;
function preload(
function preloadFactory(
componentName: 'remoteConfig'
): (
firebaseApp?: App,
settingsCallback?: (instanceFactory: App['remoteConfig']) => any
settingsCallback?: (instanceFactory: App['remoteConfig']) => void | Promise<any>
) => Promise<App['remoteConfig']>;
function preload(
function preloadFactory(
componentName: 'storage'
): (
firebaseApp?: App,
settingsCallback?: (instanceFactory: App['storage']) => any
settingsCallback?: (instanceFactory: App['storage']) => void | Promise<any>
) => Promise<App['storage']>;
function preload(componentName: ComponentName) {
return async (
function preloadFactory(componentName: ComponentName) {
return (
firebaseApp?: App,
settingsCallback?: (instanceFactory: FirebaseInstanceFactory) => any
) => {
const app = firebaseApp || useFirebaseApp();
const initialized = !!app[componentName];
if (!initialized) {
await importSDK(componentName);
}
const instanceFactory = app[componentName].bind(
app
) as FirebaseInstanceFactory;
if (initialized) {
if (settingsCallback) {
console.warn(
`${componentName} was already initialized on ${
app.name == DEFAULT_APP_NAME ? 'the default app' : app.name
}, ignoring settingsCallback`
);
}
} else if (settingsCallback) {
await Promise.resolve(settingsCallback(instanceFactory));
}
return instanceFactory;
};
) => preload(componentName, firebaseApp, settingsCallback).toPromise();
}
export const preloadAuth = preload('auth');
export const preloadAnalytics = preload('analytics');
export const preloadDatabase = preload('database');
export const preloadFirestore = preload('firestore');
export const preloadFunctions = preload('functions');
export const preloadMessaging = preload('messaging');
export const preloadPerformance = preload('performance');
export const preloadRemoteConfig = preload('remoteConfig');
export const preloadStorage = preload('storage');
function preload(
componentName: ComponentName,
firebaseApp?: App,
settingsCallback: (instanceFactory: FirebaseInstanceFactory) => any = () => {}
) {
const app = firebaseApp || useFirebaseApp();
return preloadObservable(
new Observable(emitter => {
importSDK(componentName)
.then(() => {
const instanceFactory = app[componentName].bind(
app
) as FirebaseInstanceFactory;
Promise.resolve(settingsCallback(instanceFactory)).then(() => {
emitter.next(instanceFactory);
emitter.complete();
});
})
.catch(e => {
emitter.error(e);
emitter.complete();
});
}),
`firebase-sdk:${componentName}:${app.name}`
);
}
export const preloadAuth = preloadFactory('auth');
export const preloadAnalytics = preloadFactory('analytics');
export const preloadDatabase = preloadFactory('database');
export const preloadFirestore = preloadFactory('firestore');
export const preloadFunctions = preloadFactory('functions');
export const preloadMessaging = preloadFactory('messaging');
export const preloadPerformance = preloadFactory('performance');
export const preloadRemoteConfig = preloadFactory('remoteConfig');
export const preloadStorage = preloadFactory('storage');

View File

@@ -10,9 +10,11 @@ import {
useFirestoreCollectionData,
useFirestoreDocData,
useFirestoreDocDataOnce,
useFirestoreDocOnce
useFirestoreDocOnce,
useFirestore
} from '..';
import { firestore } from 'firebase/app';
import { preloadFirestore } from '../firebaseApp';
describe('Firestore', () => {
let app: import('firebase').app.App;
@@ -41,6 +43,48 @@ describe('Firestore', () => {
.add({ a: 'hello' });
});
describe('useFirestore', () => {
it('awaits the preloadFirestore setup', async () => {
const app2 = firebase.initializeTestApp({
projectId: '123456',
databaseName: 'my-database',
auth: { uid: 'alice' }
});
let firestore: firebase.firestore.Firestore;
let preloadResolved = false;
let preloadResolve: (v?: unknown) => void;
preloadFirestore(app2, () => new Promise(resolve => preloadResolve = resolve)).then(() => preloadResolved = true);
const Firestore = () => {
const firestore = useFirestore(app2);
return (
<div data-testid="success"></div>
);
};
const { getByTestId } = render(
<FirebaseAppProvider firebase={app2}>
<React.Suspense fallback={<h1 data-testid="fallback">Fallback</h1>}>
<Firestore />
</React.Suspense>
</FirebaseAppProvider>
);
await waitForElement(() => getByTestId('fallback'));
expect(preloadResolved).toEqual(false);
await waitForElement(() => getByTestId('success')).then(() => fail('expected throw')).catch(() => {});
expect(preloadResolved).toEqual(false);
preloadResolve();
await waitForElement(() => getByTestId('success'));
expect(preloadResolved).toEqual(true);
});
});
describe('useFirestoreDoc', () => {
it('can get a Firestore document [TEST REQUIRES EMULATOR]', async () => {
const mockData = { a: 'hello' };

View File

@@ -2,8 +2,22 @@ import * as React from 'react';
import { Observable } from 'rxjs';
import { SuspenseSubject } from './SuspenseSubject';
const globalThis = function() {
if (typeof self !== 'undefined') { return self; }
if (typeof window !== 'undefined') { return window; }
if (typeof global !== 'undefined') { return global; }
throw new Error('unable to locate global object');
}();
const PRELOADED_OBSERVABLES = '_reactFirePreloadedObservables';
const DEFAULT_TIMEOUT = 30_000;
const preloadedObservables = new Map<string, SuspenseSubject<unknown>>();
// Since we're side-effect free, we need to ensure our observable cache is global
const preloadedObservables = globalThis[PRELOADED_OBSERVABLES] || new Map<string, SuspenseSubject<unknown>>();
if (!globalThis[PRELOADED_OBSERVABLES]) {
globalThis[PRELOADED_OBSERVABLES] = preloadedObservables;
}
// Starts listening to an Observable.
// Call this once you know you're going to render a

View File

@@ -5,6 +5,7 @@
"dependencies": {
"@types/jest": "^24.9.0",
"firebase": "^7.9.1",
"js-levenshtein": "^1.1.6",
"react": "0.0.0-experimental-5de5b6150",
"react-dom": "0.0.0-experimental-5de5b6150",
"react-firebaseui": "^4.0.0",

View File

@@ -7723,6 +7723,11 @@ join-path@^1.1.1:
url-join "0.0.1"
valid-url "^1"
js-levenshtein@^1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"