diff --git a/.gitignore b/.gitignore index 6b9c2b14..b93f28f6 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,9 @@ node_modules/ reactfire/docs/reactfire-metadata.json reactfire/firestore-debug.log +reactfire/firebase-debug.log reactfire/pub/** pub yarn-error.log reactfire/database-debug.log +reactfire/reactfire-*.tgz \ No newline at end of file diff --git a/reactfire/.npmignore b/reactfire/.npmignore new file mode 100644 index 00000000..954c1245 --- /dev/null +++ b/reactfire/.npmignore @@ -0,0 +1,3 @@ +cjs/index.esm-* +**/*.test.d.ts +**/*.test.js \ No newline at end of file diff --git a/reactfire/auth/auth.test.tsx b/reactfire/auth/auth.test.tsx index 546bd396..58f1e342 100644 --- a/reactfire/auth/auth.test.tsx +++ b/reactfire/auth/auth.test.tsx @@ -15,6 +15,10 @@ class MockAuth { this.subscriber = null; } + app = { + name: '[DEFAULT]' + }; + notifySubscriber() { if (this.subscriber) { this.subscriber.next(this.user); @@ -39,16 +43,16 @@ const mockFirebase = { }; const Provider = ({ children }) => ( - + {children} ); -const Component = ({ children }) => ( +const Component = (props?: { children?: any }) => ( not signed in}> - {children ||

signed in

} + {props?.children ||

signed in

}
@@ -57,12 +61,14 @@ const Component = ({ children }) => ( describe('AuthCheck', () => { beforeEach(() => { // clear the signed in user - mockFirebase.auth().updateUser(null); + act(() => mockFirebase.auth().updateUser(null)); }); afterEach(() => { - cleanup(); - jest.clearAllMocks(); + act(() => { + cleanup(); + jest.clearAllMocks(); + }); }); it('can find firebase Auth from Context', () => { @@ -95,7 +101,7 @@ describe('AuthCheck', () => { }); it('renders children if a user is logged in', async () => { - mockFirebase.auth().updateUser({ uid: 'testuser' }); + act(() => mockFirebase.auth().updateUser({ uid: 'testuser' })); const { getByTestId } = render(); await wait(() => expect(getByTestId('signed-in')).toBeInTheDocument()); diff --git a/reactfire/auth/index.tsx b/reactfire/auth/index.tsx index 4a505dba..f91c5a56 100644 --- a/reactfire/auth/index.tsx +++ b/reactfire/auth/index.tsx @@ -14,9 +14,9 @@ export function preloadUser(firebaseApp: firebase.app.App) { return preloadAuth(firebaseApp).then(auth => { const result = preloadObservable( user(auth() as firebase.auth.Auth), - 'auth: user' + `auth:user:${firebaseApp.name}` ); - return result.request.promise; + return result.toPromise(); }); } @@ -31,18 +31,8 @@ export function useUser( options?: ReactFireOptions ): User | T { auth = auth || useAuth()(); - - let currentUser = undefined; - - if (options && options.startWithValue !== undefined) { - currentUser = options.startWithValue; - } else if (auth.currentUser) { - // if auth.currentUser is undefined or null, we won't use it - // because null can mean "not signed in" OR "still loading" - currentUser = auth.currentUser; - } - - return useObservable(user(auth), 'auth: user', currentUser); + const currentUser = auth.currentUser || options?.startWithValue; + return useObservable(user(auth), `auth:user:${auth.app.name}`, currentUser); } export function useIdTokenResult(user: User, forceRefresh: boolean = false) { @@ -52,7 +42,10 @@ export function useIdTokenResult(user: User, forceRefresh: boolean = false) { const idToken$ = from(user.getIdTokenResult(forceRefresh)); - return useObservable(idToken$, `${user.uid}-claims`); + return useObservable( + idToken$, + `auth:idTokenResult:${user.uid}:forceRefresh=${forceRefresh}` + ); } export interface AuthCheckProps { diff --git a/reactfire/database/index.tsx b/reactfire/database/index.tsx index 40fcc154..77e43b5d 100644 --- a/reactfire/database/index.tsx +++ b/reactfire/database/index.tsx @@ -22,7 +22,7 @@ export function useDatabaseObject( ): QueryChange | T { return useObservable( object(ref), - `RTDB Doc: ${ref.toString()}`, + `database:object:${ref.toString()}`, options ? options.startWithValue : undefined ); } @@ -54,9 +54,10 @@ export function useDatabaseObjectData( ref: database.Reference, options?: ReactFireOptions ): T { + const idField = checkIdField(options); return useObservable( - objectVal(ref, checkIdField(options)), - `RTDB DocData: ${ref.toString()}`, + objectVal(ref, idField), + `database:objectVal:${ref.toString()}:idField=${idField}`, checkStartWithValue(options) ); } @@ -78,7 +79,7 @@ export function useDatabaseList( ref: database.Reference | database.Query, options?: ReactFireOptions ): QueryChange[] | T[] { - const hash = `RTDB List: ${ref.toString()}|${(ref as _QueryWithId).queryIdentifier()}`; + const hash = `database:list:${ref.toString()}|${(ref as _QueryWithId).queryIdentifier()}`; return useObservable( list(ref), @@ -91,9 +92,10 @@ export function useDatabaseListData( ref: database.Reference | database.Query, options?: ReactFireOptions ): T[] { + const idField = checkIdField(options); return useObservable( - listVal(ref, checkIdField(options)), - `RTDB ListData: ${ref.toString()}`, + listVal(ref, idField), + `database:listVal:${ref.toString()}|${(ref as _QueryWithId).queryIdentifier()}:idField=${idField}`, checkStartWithValue(options) ); } diff --git a/reactfire/firebaseApp/firebaseApp.test.tsx b/reactfire/firebaseApp/firebaseApp.test.tsx index ea6c2df2..e43f5908 100644 --- a/reactfire/firebaseApp/firebaseApp.test.tsx +++ b/reactfire/firebaseApp/firebaseApp.test.tsx @@ -8,21 +8,21 @@ import { FirebaseAppProvider } from './index'; afterEach(cleanup); +const DEFAULT_APP_CONFIG = { appId: '12345' }; + describe('FirebaseAppProvider', () => { it('calls firebase.initializeApp with the provided config', () => { - const config = { appId: '12345' }; - const spy = jest.spyOn(firebase, 'initializeApp'); - render(); - expect(spy).toBeCalledWith(config); + render(); + expect(spy).toBeCalledWith(DEFAULT_APP_CONFIG, undefined); spy.mockRestore(); }); it('does not call firebase.initializeApp if the firebaseApp is provided', () => { const spy = jest.spyOn(firebase, 'initializeApp'); - const app = {}; + const app: firebase.app.App = {} as any; render(); expect(spy).not.toBeCalled(); @@ -32,7 +32,7 @@ describe('FirebaseAppProvider', () => { it('initializes fireperf if specified', async () => { const mockPerf = jest.fn(); firebase['performance' as any] = mockPerf; - const app = { performance: mockPerf }; + const app: firebase.app.App = { performance: mockPerf } as any; render(); @@ -42,7 +42,7 @@ describe('FirebaseAppProvider', () => { it('does not initialize fireperf if not specified', async () => { const mockPerf = jest.fn(); firebase['performance' as any] = mockPerf; - const app = { performance: mockPerf }; + const app: firebase.app.App = { performance: mockPerf } as any; render(); @@ -52,7 +52,7 @@ describe('FirebaseAppProvider', () => { describe('useFirebaseApp', () => { it('finds firebase from Context', () => { - const firebaseApp = { a: 1 }; + const firebaseApp: firebase.app.App = { a: 1 } as any; const wrapper = ({ children }) => ( @@ -65,6 +65,57 @@ describe('useFirebaseApp', () => { expect(result.current).toBe(firebaseApp); }); + it('can initialize more than one firebase app', () => { + const config = { a: 1 }; + + const initializeApp = jest.spyOn(firebase, 'initializeApp'); + + const wrapper = ({ children }) => ( +
+ + {children} + + + appA + +
+ ); + + const { result } = renderHook(() => useFirebaseApp(), { wrapper }); + + expect(initializeApp).toBeCalledWith(config, 'app-2'); + initializeApp.mockRestore(); + + expect(result.error).toBeUndefined(); + }); + + it('will throw if configs dont match, and same name', () => { + const config = { a: 1 }; + + const initializeApp = jest.spyOn(firebase, 'initializeApp'); + + const wrapper = ({ children }) => ( +
+ + {children} + + appA +
+ ); + + try { + const { result } = renderHook(() => useFirebaseApp(), { wrapper }); + fail('expected a throw'); + } catch (e) { + expect(e).toEqual( + 'Does not match the options already provided to the default firebase app instance, give this new instance a different appName.' + ); + } + + expect(initializeApp).not.toBeCalled(); + initializeApp.mockRestore(); + }); + it('throws an error if Firebase is not in context', () => { const { result } = renderHook(() => useFirebaseApp()); diff --git a/reactfire/firebaseApp/index.tsx b/reactfire/firebaseApp/index.tsx index e8ce969b..df03183b 100644 --- a/reactfire/firebaseApp/index.tsx +++ b/reactfire/firebaseApp/index.tsx @@ -5,34 +5,54 @@ export * from './sdk'; type FirebaseAppContextValue = firebase.app.App; +// INVESTIGATE I don't like magic strings, can we have export this in js-sdk? +const DEFAULT_APP_NAME = '[DEFAULT]'; + const FirebaseAppContext = React.createContext< FirebaseAppContextValue | undefined >(undefined); -export function FirebaseAppProvider(props) { - const { firebaseConfig, initPerformance } = props; - let { firebaseApp } = props; +type Props = { + initPerformance?: boolean; + firebaseApp?: firebase.app.App; + firebaseConfig?: Object; + appName?: string; +}; + +const shallowEq = (a: Object, b: Object) => + a == b || + [...Object.keys(a), ...Object.keys(b)].every(key => a[key] == b[key]); - firebaseApp = - firebaseApp || +export function FirebaseAppProvider(props: Props & { [key: string]: unknown }) { + const { firebaseConfig, appName, initPerformance = false } = props; + const firebaseApp: firebase.app.App = + props.firebaseApp || React.useMemo(() => { - if (!firebase.apps.length) { - firebase.initializeApp(firebaseConfig); + const existingApp = firebase.apps.find( + app => app.name == (appName || DEFAULT_APP_NAME) + ); + if (existingApp) { + if (shallowEq(existingApp.options, firebaseConfig)) { + return existingApp; + } else { + throw `Does not match the options already provided to the ${appName || + 'default'} firebase app instance, give this new instance a different appName.`; + } + } else { + return firebase.initializeApp(firebaseConfig, appName); } - - return firebase; - }, [firebaseConfig]); + }, [firebaseConfig, appName]); React.useMemo(() => { - if (initPerformance === true && !!firebase.apps.length) { - if (!firebase.performance) { + if (initPerformance === true) { + if (firebaseApp.performance) { + // initialize Performance Monitoring + firebaseApp.performance(); + } else { throw new Error( 'firebase.performance not found. Did you forget to import it?' ); } - - // initialize Performance Monitoring - firebase.performance(); } }, [initPerformance, firebaseApp]); diff --git a/reactfire/firebaseApp/sdk.tsx b/reactfire/firebaseApp/sdk.tsx index eb119b7b..90a67c54 100644 --- a/reactfire/firebaseApp/sdk.tsx +++ b/reactfire/firebaseApp/sdk.tsx @@ -1,4 +1,6 @@ -import { useFirebaseApp, preloadRequest, usePreloadedRequest } from '..'; +import { useFirebaseApp } from '..'; +import { preloadObservable, useObservable } from '../useObservable'; +import { from } from 'rxjs'; type RemoteConfig = import('firebase/app').remoteConfig.RemoteConfig; type Storage = import('firebase/app').storage.Storage; @@ -98,7 +100,10 @@ function fetchSDK( .then(() => settingsCallback(firebaseApp[sdk])) .then(() => firebaseApp[sdk]); } - preloadRequest(() => sdkPromise, `firebase-sdk-${sdk}`); + preloadObservable( + from(sdkPromise), + `firebase:sdk-${sdk}:${firebaseApp.name}` + ); return sdkPromise; } @@ -107,12 +112,10 @@ function useSDK(sdk: SDK, firebaseApp?: firebase.app.App) { firebaseApp = firebaseApp || useFirebaseApp(); // use the request cache so we don't issue multiple fetches for the sdk - const result = preloadRequest( - () => fetchSDK(sdk, firebaseApp), - `firebase-sdk-${sdk}` + return useObservable( + from(fetchSDK(sdk, firebaseApp)), + `firebase:sdk-${sdk}:${firebaseApp.name}` ); - - return usePreloadedRequest(result); } export function preloadAuth( @@ -123,7 +126,7 @@ export function preloadAuth( } export function useAuth(firebaseApp?: firebase.app.App) { - return useSDK(SDK.AUTH, firebaseApp); + return useSDK(SDK.AUTH, firebaseApp) as () => firebase.auth.Auth; } export function preloadAnalytics(firebaseApp: firebase.app.App) { @@ -197,7 +200,10 @@ export function preloadRemoteConfig( } export function useRemoteConfig(firebaseApp?: firebase.app.App) { - return useSDK(SDK.REMOTE_CONFIG, firebaseApp); + return useSDK( + SDK.REMOTE_CONFIG, + firebaseApp + ) as () => firebase.remoteConfig.RemoteConfig; } export function preloadStorage( diff --git a/reactfire/firestore/firestore.test.tsx b/reactfire/firestore/firestore.test.tsx index c3a781ee..d1637697 100644 --- a/reactfire/firestore/firestore.test.tsx +++ b/reactfire/firestore/firestore.test.tsx @@ -115,7 +115,7 @@ describe('Firestore', () => { await act(() => ref.add(mockData2)); const ReadFirestoreCollection = () => { - const collection = useFirestoreCollection(ref); + const collection = useFirestoreCollection(ref) as any; return (
    @@ -151,11 +151,12 @@ describe('Firestore', () => { await act(() => ref.add(mockData2)); const ReadFirestoreCollection = () => { - const list = (useFirestoreCollection(ref) as firestore.QuerySnapshot) - .docs; - const filteredList = (useFirestoreCollection( + const list = ((useFirestoreCollection( + ref + ) as any) as firestore.QuerySnapshot).docs; + const filteredList = ((useFirestoreCollection( filteredRef - ) as firestore.QuerySnapshot).docs; + ) as any) as firestore.QuerySnapshot).docs; // filteredList's length should be 1 since we only added one value that matches its query expect(filteredList.length).toEqual(1); diff --git a/reactfire/firestore/index.tsx b/reactfire/firestore/index.tsx index da5ee8e7..1da73f0e 100644 --- a/reactfire/firestore/index.tsx +++ b/reactfire/firestore/index.tsx @@ -28,7 +28,10 @@ export function preloadFirestoreDoc( ) { return preloadFirestore(firebaseApp).then(firestore => { const ref = refProvider(firestore() as firebase.firestore.Firestore); - return preloadObservable(doc(ref), ref.path); + return preloadObservable( + doc(ref), + `firestore:doc:${ref.firestore.app.name}:${ref.path}` + ); }); } @@ -44,7 +47,7 @@ export function useFirestoreDoc( ): T extends {} ? T : firestore.DocumentSnapshot { return useObservable( doc(ref), - 'firestore doc: ' + ref.path, + `firestore:doc:${ref.firestore.app.name}:${ref.path}`, options ? options.startWithValue : undefined ); } @@ -59,9 +62,10 @@ export function useFirestoreDocData( ref: firestore.DocumentReference, options?: ReactFireOptions ): T { + const idField = checkIdField(options); return useObservable( - docData(ref, checkIdField(options)), - 'firestore docdata: ' + ref.path, + docData(ref, idField), + `firestore:docData:${ref.firestore.app.name}:${ref.path}:idField=${idField}`, checkStartWithValue(options) ); } @@ -76,10 +80,12 @@ export function useFirestoreCollection( query: firestore.Query, options?: ReactFireOptions ): T extends {} ? T[] : firestore.QuerySnapshot { - const queryId = getHashFromFirestoreQuery(query); + const queryId = `firestore:collection:${ + query.firestore.app.name + }:${getHashFromFirestoreQuery(query)}`; return useObservable( - fromCollectionRef(query, checkIdField(options)), + fromCollectionRef(query), queryId, options ? options.startWithValue : undefined ); @@ -96,8 +102,7 @@ interface _QueryWithId extends firestore.Query { } function getHashFromFirestoreQuery(query: firestore.Query) { - const hash = (query as _QueryWithId)._query.canonicalId(); - return `firestore: ${hash}`; + return (query as _QueryWithId)._query.canonicalId(); } /** @@ -110,10 +115,13 @@ export function useFirestoreCollectionData( query: firestore.Query, options?: ReactFireOptions ): T[] { - const queryId = getHashFromFirestoreQuery(query); + const idField = checkIdField(options); + const queryId = `firestore:collectionData:${ + query.firestore.app.name + }:${getHashFromFirestoreQuery(query)}:idField=${idField}`; return useObservable( - collectionData(query, checkIdField(options)), + collectionData(query, idField), queryId, checkStartWithValue(options) ); diff --git a/reactfire/package.json b/reactfire/package.json index 3d7b96eb..9f18eeb8 100644 --- a/reactfire/package.json +++ b/reactfire/package.json @@ -9,10 +9,10 @@ "build-dev": "tsc --watch", "test-dev": "jest --verbose --watch", "emulators": "firebase emulators:start --only firestore,database", - "test": "firebase emulators:exec --only firestore,database \"jest --no-cache --verbose --detectOpenHandles --forceExit\"", + "test": "yarn build && firebase emulators:exec --only firestore,database \"jest --rootDir pub --no-cache --verbose --detectOpenHandles --forceExit\"", "copy-package-json": "cp package.pub.json pub/reactfire/package.json", "watch": "yarn build && tsc --watch", - "build": "rm -rf pub && tsc && yarn copy-package-json && cp ../README.md pub/reactfire/README.md && cp ../LICENSE pub/reactfire/LICENSE && rollup -c" + "build": "rm -rf pub && tsc && yarn copy-package-json && cp ../README.md pub/reactfire/README.md && cp ../LICENSE pub/reactfire/LICENSE && rollup -c && cp ./.npmignore pub/reactfire/ && npm pack ./pub/reactfire" }, "repository": { "type": "git", diff --git a/reactfire/performance/performance.test.tsx b/reactfire/performance/performance.test.tsx index 14d3caa2..c977ded4 100644 --- a/reactfire/performance/performance.test.tsx +++ b/reactfire/performance/performance.test.tsx @@ -18,9 +18,9 @@ const mockPerf = jest.fn(() => { return { trace: createTrace }; }); -const mockFirebase = { +const mockFirebase: firebase.app.App = { performance: mockPerf -}; +} as any; const PromiseThrower = () => { throw new Promise((resolve, reject) => {}); @@ -45,7 +45,7 @@ describe('SuspenseWithPerf', () => { const Fallback = () =>

    Fallback

    ; const Comp = () => { - useObservable(o$, 'test'); + useObservable(o$, 'perf-test-1'); return

    Actual

    ; }; @@ -193,7 +193,7 @@ describe('SuspenseWithPerf', () => { const o$ = new Subject(); const Comp = () => { - const val = useObservable(o$, 'test'); + const val = useObservable(o$, 'perf-test-2'); if (val === 'throw') { throw new Promise(() => {}); diff --git a/reactfire/remote-config/index.tsx b/reactfire/remote-config/index.tsx index 9bdc7938..b09fdccf 100644 --- a/reactfire/remote-config/index.tsx +++ b/reactfire/remote-config/index.tsx @@ -14,6 +14,11 @@ type RemoteConfig = import('firebase/app').remoteConfig.RemoteConfig; type RemoteConfigValue = import('firebase/app').remoteConfig.Value; type Getter$ = (remoteConfig: RemoteConfig, key: string) => Observable; +interface RemoteConfigWithPrivate extends firebase.remoteConfig.RemoteConfig { + // This is a private API, assume optional + _storage?: { appName: string }; +} + /** * Helper function to construct type safe functions. Since Remote Config has * methods that return different types for values, we need to be extra safe @@ -28,8 +33,14 @@ function typeSafeUse( remoteConfig?: RemoteConfig ): T { remoteConfig = remoteConfig || useRemoteConfig()(); + // INVESTIGATE need to use a public API to get at the app name, one doesn't appear to exist... + // we might need to iterate over the Firebase apps and check for remoteConfig equality? this works for now + const appName = (remoteConfig as RemoteConfigWithPrivate)._storage?.appName; const $value = getter(remoteConfig, key); - return useObservable($value, `remoteconfig:${key}`); + return useObservable( + $value, + `remoteConfig:${key}:${getter.name}:${appName}` + ); } /** diff --git a/reactfire/rollup.config.js b/reactfire/rollup.config.js index 81792c78..ea656c44 100644 --- a/reactfire/rollup.config.js +++ b/reactfire/rollup.config.js @@ -15,7 +15,8 @@ export default { 'rxfire/firestore', 'rxfire/storage', 'rxjs', - 'rxjs/operators' + 'rxjs/operators', + 'tslib' ], plugins: [resolve()] }; diff --git a/reactfire/storage/index.tsx b/reactfire/storage/index.tsx index 3852333d..5d4c248f 100644 --- a/reactfire/storage/index.tsx +++ b/reactfire/storage/index.tsx @@ -39,7 +39,7 @@ export function useStorageTask( ): storage.UploadTaskSnapshot | T { return useObservable( _fromTask(task), - 'storage upload: ' + ref.toString(), + `storage:task:${ref.toString()}`, options ? options.startWithValue : undefined ); } @@ -56,7 +56,7 @@ export function useStorageDownloadURL( ): string | T { return useObservable( getDownloadURL(ref), - 'storage download:' + ref.toString(), + `storage:downloadUrl:${ref.toString()}`, options ? options.startWithValue : undefined ); } diff --git a/reactfire/tsconfig.json b/reactfire/tsconfig.json index af5b69de..a2375310 100644 --- a/reactfire/tsconfig.json +++ b/reactfire/tsconfig.json @@ -10,6 +10,5 @@ "outDir": "pub/reactfire", "declaration": true, "skipLibCheck": true - }, - "files": ["index.ts"] + } } diff --git a/reactfire/useObservable/SuspenseSubject.ts b/reactfire/useObservable/SuspenseSubject.ts new file mode 100644 index 00000000..25e338ae --- /dev/null +++ b/reactfire/useObservable/SuspenseSubject.ts @@ -0,0 +1,90 @@ +import { Observable, Subject, Subscription, Subscriber, empty } from 'rxjs'; +import { tap, share, catchError } from 'rxjs/operators'; + +export class SuspenseSubject extends Subject { + private _value: T | undefined; + private _hasValue = false; + private _timeoutHandler: NodeJS.Timeout; + private _innerSubscriber: Subscription; + private _firstEmission: Promise; + private _resolveFirstEmission: () => void; + private _error: any = undefined; + private _innerObservable: Observable; + private _warmupSubscription: Subscription; + + constructor(innerObservable: Observable, private _timeoutWindow: number) { + super(); + this._firstEmission = new Promise( + resolve => (this._resolveFirstEmission = resolve) + ); + this._innerObservable = innerObservable.pipe( + tap( + v => { + this._next(v); + }, + e => { + // save the error, so that we can raise on subscription or .value + // resolve the promise, so suspense tries again + this._error = e; + this._resolveFirstEmission(); + } + ), + catchError(() => empty()), + share() + ); + // warm up the observable + this._warmupSubscription = this._innerObservable.subscribe(); + + // set a timeout for reseting the cache, subscriptions will cancel the timeout + // and reschedule again on unsubscribe + this._timeoutHandler = setTimeout(this._reset, this._timeoutWindow); + } + + get hasValue(): boolean { + // hasValue returns true if there's an error too + // so that after we resolve the promise & useObservable is called again + // we won't throw again + return this._hasValue || !!this._error; + } + + get value(): T { + // TODO figure out how to reset the cache here, if I _reset() here before throwing + // it doesn't seem to work. + // As it is now, this will burn the cache entry until the timeout fires. + if (this._error) { + throw this._error; + } + return this._value; + } + + get firstEmission(): Promise { + return this._firstEmission; + } + + private _next(value: T) { + this._hasValue = true; + this._value = value; + this._resolveFirstEmission(); + } + + private _reset() { + // seems to be undefined in tests? + if (this._warmupSubscription) { + this._warmupSubscription.unsubscribe(); + } + this._hasValue = false; + this._value = undefined; + this._error = undefined; + this._firstEmission = new Promise( + resolve => (this._resolveFirstEmission = resolve) + ); + } + + _subscribe(subscriber: Subscriber): Subscription { + if (this._timeoutHandler) { + clearTimeout(this._timeoutHandler); + } + this._innerSubscriber = this._innerObservable.subscribe(subscriber); + return this._innerSubscriber.add(this._reset); + } +} diff --git a/reactfire/useObservable/index.ts b/reactfire/useObservable/index.ts index f416623e..3ae892ae 100644 --- a/reactfire/useObservable/index.ts +++ b/reactfire/useObservable/index.ts @@ -1,89 +1,47 @@ import * as React from 'react'; import { Observable } from 'rxjs'; -import { first, startWith } from 'rxjs/operators'; -import { ActiveRequest, ObservablePromiseCache } from './requestCache'; +import { SuspenseSubject } from './SuspenseSubject'; -const requestCache = new ObservablePromiseCache(); - -export function preloadRequest( - getPromise, - requestId: string -): { requestId: string; request: ActiveRequest } { - const request = requestCache.createDedupedRequest(getPromise, requestId); - - return { - requestId: requestId, - request - }; -} +const DEFAULT_TIMEOUT = 30_000; +const preloadedObservables = new Map>(); // Starts listening to an Observable. // Call this once you know you're going to render a // child that will consume the observable -export function preloadObservable( - observable$: Observable, - observableId: string -): { requestId: string; request: ActiveRequest } { - return preloadRequest( - () => observable$.pipe(first()).toPromise(), - observableId - ); -} - -export function usePreloadedRequest(preloadResult: { requestId: string }) { - const request = requestCache.getRequest(preloadResult.requestId); - - // Suspend if we're not ready yet - if (!request.isComplete) { - throw request.promise; - } - - if (request.error) { - throw request.error; +export function preloadObservable(source: Observable, id: string) { + if (preloadedObservables.has(id)) { + return preloadedObservables.get(id) as SuspenseSubject; + } else { + const observable = new SuspenseSubject(source, DEFAULT_TIMEOUT); + preloadedObservables.set(id, observable); + return observable; } - - return request.value; } export function useObservable( - observable$: Observable, + source: Observable, observableId: string, - startWithValue?: T | any + startWithValue?: T | any, + deps: React.DependencyList = [observableId] ): T { if (!observableId) { throw new Error('cannot call useObservable without an observableId'); } - - const result = preloadObservable(observable$, observableId); - - let initialValue = startWithValue; - if (initialValue === undefined) { - // this will Suspend until the Promise resolves - initialValue = usePreloadedRequest(result); + const observable = preloadObservable(source, observableId); + if (!observable.hasValue && !startWithValue) { + throw observable.firstEmission; } - - const [latestValue, setValue] = React.useState(initialValue); - + const [latest, setValue] = React.useState(() => + observable.hasValue ? observable.value : startWithValue + ); React.useEffect(() => { - const subscription = observable$.pipe(startWith(initialValue)).subscribe( - newVal => { - // update the value in requestCache - result.request.setValue(newVal); - - // update state - setValue(newVal); - }, - error => { - console.error('There was an error', error); - throw error; + const subscription = observable.subscribe( + v => setValue(() => v), + e => { + throw e; } ); - - return () => { - subscription.unsubscribe(); - requestCache.removeRequest(observableId); - }; - }, [observableId]); - - return latestValue; + return () => subscription.unsubscribe(); + }, deps); + return latest; } diff --git a/reactfire/useObservable/requestCache.ts b/reactfire/useObservable/requestCache.ts deleted file mode 100644 index 526bd5e0..00000000 --- a/reactfire/useObservable/requestCache.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { first, take } from 'rxjs/operators'; -import { Observable } from 'rxjs'; - -export class ActiveRequest { - promise: Promise; - isComplete: boolean; - value: any; - error: Error; - - constructor(promise) { - this.isComplete = false; - this.promise = promise - .then(result => { - this.setValue(result); - return result; - }) - .catch(err => { - this.isComplete = true; - this.setError(err); - }); - } - - setValue = value => { - this.value = value; - this.isComplete = true; - }; - - setError = err => { - this.error = err; - this.isComplete = true; - }; -} - -/* - * this will probably be replaced by something - * like react-cache (https://www.npmjs.com/package/react-cache) - * once that is stable. - * - * Full Suspense roadmap: https://reactjs.org/blog/2018/11/27/react-16-roadmap.html - */ -export class ObservablePromiseCache { - activeRequests: Map; - - constructor() { - this.activeRequests = new Map(); - } - - getRequest(requestId) { - const request = this.activeRequests.get(requestId); - if (request === undefined) { - throw new Error(`No request with ID "${requestId}" exists`); - } - return request; - } - - createRequest(promise: Promise, requestId): ActiveRequest { - if (this.activeRequests.get(requestId) !== undefined) { - throw new Error(`request "${requestId}" is already in use.`); - } - - const request = new ActiveRequest(promise); - this.activeRequests.set(requestId, request); - - return request; - } - - createDedupedRequest(getPromise: () => Promise, requestId) { - let request = this.activeRequests.get(requestId); - - if (request === undefined) { - request = this.createRequest(getPromise(), requestId); - } - - return request; - } - - removeRequest(requestId: string) { - this.activeRequests.delete(requestId); - } -} diff --git a/reactfire/useObservable/useObservable.test.tsx b/reactfire/useObservable/useObservable.test.tsx index 7378b76d..6d7bfe16 100644 --- a/reactfire/useObservable/useObservable.test.tsx +++ b/reactfire/useObservable/useObservable.test.tsx @@ -2,7 +2,7 @@ import '@testing-library/jest-dom/extend-expect'; import { act, cleanup, render, waitForElement } from '@testing-library/react'; import { act as actOnHook, renderHook } from '@testing-library/react-hooks'; import * as React from 'react'; -import { of, Subject, BehaviorSubject, throwError } from 'rxjs'; +import { of, Subject, throwError } from 'rxjs'; import { useObservable } from '.'; describe('useObservable', () => { @@ -13,6 +13,7 @@ describe('useObservable', () => { try { useObservable(observable$, 'test'); + fail('expected a throw'); } catch (thingThatWasThrown) { expect(thingThatWasThrown).toBeInstanceOf(Promise); } @@ -23,6 +24,7 @@ describe('useObservable', () => { try { useObservable(observable$, undefined); + fail('expected a throw'); } catch (thingThatWasThrown) { expect(thingThatWasThrown).toBeInstanceOf(Error); } @@ -31,10 +33,10 @@ describe('useObservable', () => { it('can return a startval and then the observable once it is ready', () => { const startVal = 'howdy'; const observableVal = "y'all"; - const observable$: Subject = new Subject(); + const observable$ = new Subject(); const { result, waitForNextUpdate } = renderHook(() => - useObservable(observable$, 'test', startVal) + useObservable(observable$, 'test-2', startVal) ); expect(result.current).toEqual(startVal); @@ -96,25 +98,14 @@ describe('useObservable', () => { spy.mockRestore(); }); - it('returns the provided startWithValue first even if the observable is ready right away', () => { - // This behavior is a consequense of how observables work. There is - // not a synchronous way to ask an observable if it has a value to emit. - + it('provides the value, rather than startWithValue, when the observable is ready right away', () => { const startVal = 'howdy'; const observableVal = "y'all"; const observable$ = of(observableVal); - let hasReturnedStartWithValue = false; const Component = () => { - const val = useObservable(observable$, 'test', startVal); - - if (hasReturnedStartWithValue) { - expect(val).toEqual(observableVal); - } else { - expect(val).toEqual(startVal); - hasReturnedStartWithValue = true; - } - + const val = useObservable(observable$, 'test-3', startVal); + expect(val).toEqual(observableVal); return

    Hello

    ; }; @@ -123,7 +114,7 @@ describe('useObservable', () => { it('works with Suspense', async () => { const observableFinalVal = "y'all"; - const observable$ = new BehaviorSubject(undefined); + const observable$ = new Subject(); const actualComponentId = 'actual-component'; const fallbackComponentId = 'fallback-component'; @@ -163,7 +154,7 @@ describe('useObservable', () => { const observable$ = new Subject(); const { result } = renderHook(() => - useObservable(observable$, 'test', startVal) + useObservable(observable$, 'test-changes', startVal) ); expect(result.current).toEqual(startVal); diff --git a/yarn.lock b/yarn.lock index 0609d1c0..0f6cdb83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25,6 +25,15 @@ invariant "^2.2.4" semver "^5.5.0" +"@babel/compat-data@^7.8.4": + version "7.8.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.8.5.tgz#d28ce872778c23551cbb9432fc68d28495b613b9" + integrity sha512-jWYUqQX/ObOhG1UiEkbH5SANsE/8oKXiQWjj7p7xgj9Zmnt//aUvyz4dBkK0HNsS8/cbyC5NmmH87VekW+mXFg== + dependencies: + browserslist "^4.8.5" + invariant "^2.2.4" + semver "^5.5.0" + "@babel/core@7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.7.4.tgz#37e864532200cb6b50ee9a4045f5f817840166ab" @@ -45,7 +54,7 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/core@^7.1.0", "@babel/core@^7.4.3", "@babel/core@^7.4.5": +"@babel/core@^7.1.0", "@babel/core@^7.4.5": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.8.3.tgz#30b0ebb4dd1585de6923a0b4d179e0b9f5d82941" integrity sha512-4XFkf8AwyrEG7Ziu3L2L0Cv+WyY47Tcsp70JFmpftbAA1K7YL/sgE9jh9HyNj08Y/U50ItUchpN0w6HxAoX1rA== @@ -66,6 +75,27 @@ semver "^5.4.1" source-map "^0.5.0" +"@babel/core@^7.4.3": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.8.4.tgz#d496799e5c12195b3602d0fddd77294e3e38e80e" + integrity sha512-0LiLrB2PwrVI+a2/IEskBopDYSd8BCb3rOvH7D5tzoWd696TBEduBvuLVm4Nx6rltrLZqvI3MCalB2K2aVzQjA== + dependencies: + "@babel/code-frame" "^7.8.3" + "@babel/generator" "^7.8.4" + "@babel/helpers" "^7.8.4" + "@babel/parser" "^7.8.4" + "@babel/template" "^7.8.3" + "@babel/traverse" "^7.8.4" + "@babel/types" "^7.8.3" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.1" + json5 "^2.1.0" + lodash "^4.17.13" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + "@babel/generator@^7.4.0", "@babel/generator@^7.7.4", "@babel/generator@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.8.3.tgz#0e22c005b0a94c1c74eafe19ef78ce53a4d45c03" @@ -76,6 +106,16 @@ lodash "^4.17.13" source-map "^0.5.0" +"@babel/generator@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.8.4.tgz#35bbc74486956fe4251829f9f6c48330e8d0985e" + integrity sha512-PwhclGdRpNAf3IxZb0YVuITPZmmrXz9zf6fH8lT4XbrmfQKr6ryBzhv593P5C6poJRciFCL/eHGW2NuGrgEyxA== + dependencies: + "@babel/types" "^7.8.3" + jsesc "^2.5.1" + lodash "^4.17.13" + source-map "^0.5.0" + "@babel/helper-annotate-as-pure@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz#60bc0bc657f63a0924ff9a4b4a0b24a13cf4deee" @@ -119,6 +159,17 @@ levenary "^1.1.0" semver "^5.5.0" +"@babel/helper-compilation-targets@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.8.4.tgz#03d7ecd454b7ebe19a254f76617e61770aed2c88" + integrity sha512-3k3BsKMvPp5bjxgMdrFyq0UaEO48HciVrOVF0+lon8pp95cyJ2ujAh0TrBHNMnJGT2rr0iKOJPFFbSqjDyf/Pg== + dependencies: + "@babel/compat-data" "^7.8.4" + browserslist "^4.8.5" + invariant "^2.2.4" + levenary "^1.1.1" + semver "^5.5.0" + "@babel/helper-create-class-features-plugin@^7.7.4", "@babel/helper-create-class-features-plugin@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.8.3.tgz#5b94be88c255f140fd2c10dd151e7f98f4bff397" @@ -279,6 +330,15 @@ "@babel/traverse" "^7.8.3" "@babel/types" "^7.8.3" +"@babel/helpers@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.8.4.tgz#754eb3ee727c165e0a240d6c207de7c455f36f73" + integrity sha512-VPbe7wcQ4chu4TDQjimHv/5tj73qz88o12EPkO2ValS2QiQS/1F2SsjyIGNnAD0vF/nZS6Cf9i+vW6HIlnaR8w== + dependencies: + "@babel/template" "^7.8.3" + "@babel/traverse" "^7.8.4" + "@babel/types" "^7.8.3" + "@babel/highlight@^7.0.0", "@babel/highlight@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.8.3.tgz#28f173d04223eaaa59bc1d439a3836e6d1265797" @@ -293,6 +353,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.8.3.tgz#790874091d2001c9be6ec426c2eed47bc7679081" integrity sha512-/V72F4Yp/qmHaTALizEm9Gf2eQHV3QyTL3K0cNfijwnMnb1L+LDlAubb/ZnSdGAVzVSWakujHYs1I26x66sMeQ== +"@babel/parser@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.8.4.tgz#d1dbe64691d60358a974295fa53da074dd2ce8e8" + integrity sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw== + "@babel/plugin-proposal-async-generator-functions@^7.7.4", "@babel/plugin-proposal-async-generator-functions@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.8.3.tgz#bad329c670b382589721b27540c7d288601c6e6f" @@ -609,6 +674,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" +"@babel/plugin-transform-for-of@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.8.4.tgz#6fe8eae5d6875086ee185dd0b098a8513783b47d" + integrity sha512-iAXNlOWvcYUYoV8YIxwS7TxGRJcxyl8eQCfT+A5j8sKUzRFvJdcyjp97jL2IghWSRDaL2PU2O2tX8Cu9dTBq5A== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-transform-function-name@^7.7.4", "@babel/plugin-transform-function-name@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.8.3.tgz#279373cb27322aaad67c2683e776dfc47196ed8b" @@ -699,6 +771,15 @@ "@babel/helper-get-function-arity" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" +"@babel/plugin-transform-parameters@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.8.4.tgz#1d5155de0b65db0ccf9971165745d3bb990d77d3" + integrity sha512-IsS3oTxeTsZlE5KqzTbcC2sV0P9pXdec53SU+Yxv7o/6dvGM5AkTotQKhoSffhNgZ/dftsSiOoxy7evCYJXzVA== + dependencies: + "@babel/helper-call-delegate" "^7.8.3" + "@babel/helper-get-function-arity" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-transform-property-literals@^7.7.4", "@babel/plugin-transform-property-literals@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.8.3.tgz#33194300d8539c1ed28c62ad5087ba3807b98263" @@ -814,6 +895,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" +"@babel/plugin-transform-typeof-symbol@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.8.4.tgz#ede4062315ce0aaf8a657a920858f1a2f35fc412" + integrity sha512-2QKyfjGdvuNfHsb7qnBBlKclbD4CfshH2KvDabiijLMGXPHJXGxtDzwIF7bQP+T0ysw8fYTtxPafgfs/c1Lrqg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-transform-typescript@^7.7.4", "@babel/plugin-transform-typescript@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.8.3.tgz#be6f01a7ef423be68e65ace1f04fc407e6d88917" @@ -888,7 +976,70 @@ js-levenshtein "^1.1.3" semver "^5.5.0" -"@babel/preset-env@^7.4.3", "@babel/preset-env@^7.4.5": +"@babel/preset-env@^7.4.3": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.8.4.tgz#9dac6df5f423015d3d49b6e9e5fa3413e4a72c4e" + integrity sha512-HihCgpr45AnSOHRbS5cWNTINs0TwaR8BS8xIIH+QwiW8cKL0llV91njQMpeMReEPVs+1Ao0x3RLEBLtt1hOq4w== + dependencies: + "@babel/compat-data" "^7.8.4" + "@babel/helper-compilation-targets" "^7.8.4" + "@babel/helper-module-imports" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-proposal-async-generator-functions" "^7.8.3" + "@babel/plugin-proposal-dynamic-import" "^7.8.3" + "@babel/plugin-proposal-json-strings" "^7.8.3" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-proposal-object-rest-spread" "^7.8.3" + "@babel/plugin-proposal-optional-catch-binding" "^7.8.3" + "@babel/plugin-proposal-optional-chaining" "^7.8.3" + "@babel/plugin-proposal-unicode-property-regex" "^7.8.3" + "@babel/plugin-syntax-async-generators" "^7.8.0" + "@babel/plugin-syntax-dynamic-import" "^7.8.0" + "@babel/plugin-syntax-json-strings" "^7.8.0" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" + "@babel/plugin-syntax-object-rest-spread" "^7.8.0" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" + "@babel/plugin-syntax-optional-chaining" "^7.8.0" + "@babel/plugin-syntax-top-level-await" "^7.8.3" + "@babel/plugin-transform-arrow-functions" "^7.8.3" + "@babel/plugin-transform-async-to-generator" "^7.8.3" + "@babel/plugin-transform-block-scoped-functions" "^7.8.3" + "@babel/plugin-transform-block-scoping" "^7.8.3" + "@babel/plugin-transform-classes" "^7.8.3" + "@babel/plugin-transform-computed-properties" "^7.8.3" + "@babel/plugin-transform-destructuring" "^7.8.3" + "@babel/plugin-transform-dotall-regex" "^7.8.3" + "@babel/plugin-transform-duplicate-keys" "^7.8.3" + "@babel/plugin-transform-exponentiation-operator" "^7.8.3" + "@babel/plugin-transform-for-of" "^7.8.4" + "@babel/plugin-transform-function-name" "^7.8.3" + "@babel/plugin-transform-literals" "^7.8.3" + "@babel/plugin-transform-member-expression-literals" "^7.8.3" + "@babel/plugin-transform-modules-amd" "^7.8.3" + "@babel/plugin-transform-modules-commonjs" "^7.8.3" + "@babel/plugin-transform-modules-systemjs" "^7.8.3" + "@babel/plugin-transform-modules-umd" "^7.8.3" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.8.3" + "@babel/plugin-transform-new-target" "^7.8.3" + "@babel/plugin-transform-object-super" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.8.4" + "@babel/plugin-transform-property-literals" "^7.8.3" + "@babel/plugin-transform-regenerator" "^7.8.3" + "@babel/plugin-transform-reserved-words" "^7.8.3" + "@babel/plugin-transform-shorthand-properties" "^7.8.3" + "@babel/plugin-transform-spread" "^7.8.3" + "@babel/plugin-transform-sticky-regex" "^7.8.3" + "@babel/plugin-transform-template-literals" "^7.8.3" + "@babel/plugin-transform-typeof-symbol" "^7.8.4" + "@babel/plugin-transform-unicode-regex" "^7.8.3" + "@babel/types" "^7.8.3" + browserslist "^4.8.5" + core-js-compat "^3.6.2" + invariant "^2.2.2" + levenary "^1.1.1" + semver "^5.5.0" + +"@babel/preset-env@^7.4.5": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.8.3.tgz#dc0fb2938f52bbddd79b3c861a4b3427dd3a6c54" integrity sha512-Rs4RPL2KjSLSE2mWAx5/iCH+GC1ikKdxPrhnRS6PfFVaiZeom22VFKN4X8ZthyN61kAaR05tfXTbCvatl9WIQg== @@ -1035,6 +1186,21 @@ globals "^11.1.0" lodash "^4.17.13" +"@babel/traverse@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.8.4.tgz#f0845822365f9d5b0e312ed3959d3f827f869e3c" + integrity sha512-NGLJPZwnVEyBPLI+bl9y9aSnxMhsKz42so7ApAv9D+b4vAFPpY013FTS9LdKxcABoIYFU52HcYga1pPlx454mg== + dependencies: + "@babel/code-frame" "^7.8.3" + "@babel/generator" "^7.8.4" + "@babel/helper-function-name" "^7.8.3" + "@babel/helper-split-export-declaration" "^7.8.3" + "@babel/parser" "^7.8.4" + "@babel/types" "^7.8.3" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.13" + "@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.7.4", "@babel/types@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.3.tgz#5a383dffa5416db1b73dedffd311ffd0788fb31c" @@ -3348,6 +3514,15 @@ browserslist@^4.0.0, browserslist@^4.6.0, browserslist@^4.6.2, browserslist@^4.6 electron-to-chromium "^1.3.338" node-releases "^1.1.46" +browserslist@^4.8.5: + version "4.8.6" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.8.6.tgz#96406f3f5f0755d272e27a66f4163ca821590a7e" + integrity sha512-ZHao85gf0eZ0ESxLfCp73GG9O/VTytYDIkIiZDlURppLTI9wErSM/5yAKEq6rcUdxBLjMELmrYUJGg5sxGKMHg== + dependencies: + caniuse-lite "^1.0.30001023" + electron-to-chromium "^1.3.341" + node-releases "^1.1.47" + bser@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" @@ -3566,6 +3741,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001010, can resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001023.tgz#b82155827f3f5009077bdd2df3d8968bcbcc6fc4" integrity sha512-C5TDMiYG11EOhVOA62W1p3UsJ2z4DsHtMBQtjzp3ZsUglcQn62WOUgW0y795c7A5uZ+GCEIvzkMatLIlAsbNTA== +caniuse-lite@^1.0.30001023: + version "1.0.30001025" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001025.tgz#30336a8aca7f98618eb3cf38e35184e13d4e5fe6" + integrity sha512-SKyFdHYfXUZf5V85+PJgLYyit27q4wgvZuf8QTOk1osbypcROihMBlx9GRar2/pIcKH2r4OehdlBr9x6PXetAQ== + capture-exit@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" @@ -4969,6 +5149,11 @@ electron-to-chromium@^1.3.306, electron-to-chromium@^1.3.338: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.341.tgz#ad4c039bf621715a12dd814a95a7d89ec80b092c" integrity sha512-iezlV55/tan1rvdvt7yg7VHRSkt+sKfzQ16wTDqTbQqtl4+pSUkKPXpQHDvEt0c7gKcUHHwUbffOgXz6bn096g== +electron-to-chromium@^1.3.341: + version "1.3.345" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.345.tgz#2569d0d54a64ef0f32a4b7e8c80afa5fe57c5d98" + integrity sha512-f8nx53+Z9Y+SPWGg3YdHrbYYfIJAtbUjpFfW4X1RwTZ94iUG7geg9tV8HqzAXX7XTNgyWgAFvce4yce8ZKxKmg== + elliptic@^6.0.0: version "6.5.2" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.2.tgz#05c5678d7173c049d8ca433552224a495d0e3762" @@ -8247,7 +8432,7 @@ leven@^3.1.0: resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== -levenary@^1.1.0: +levenary@^1.1.0, levenary@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/levenary/-/levenary-1.1.1.tgz#842a9ee98d2075aa7faeedbe32679e9205f46f77" integrity sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ== @@ -9207,6 +9392,13 @@ node-releases@^1.1.40, node-releases@^1.1.46: dependencies: semver "^6.3.0" +node-releases@^1.1.47: + version "1.1.48" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.48.tgz#7f647f0c453a0495bcd64cbd4778c26035c2f03a" + integrity sha512-Hr8BbmUl1ujAST0K0snItzEA5zkJTQup8VNTKNfT6Zw8vTJkIiagUPNfxHmgDOyfFYNfKAul40sD0UEYTvwebw== + dependencies: + semver "^6.3.0" + nopt@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"