reactfire
Version:
Firebase library for React
139 lines (123 loc) • 4.62 kB
text/typescript
import { empty, Observable, Subject, Subscriber, Subscription } from 'rxjs';
import { catchError, shareReplay, tap } from 'rxjs/operators';
import { ObservableStatus } from './useObservable';
export class SuspenseSubject<T> extends Subject<T> {
private _value: T | undefined;
private _hasValue = false;
private _timeoutHandler: ReturnType<typeof setTimeout>;
private _firstEmission: Promise<void>;
private _error: any = undefined;
private _innerObservable: Observable<T>;
private _warmupSubscription: Subscription;
private _immutableStatus: ObservableStatus<T>;
private _isComplete = false;
// @ts-expect-error: TODO: double check to see if this is an RXJS thing or if we should listen to TS
private _innerSubscriber: Subscription;
// @ts-expect-error: TODO: double check to see if this is an RXJS thing or if we should listen to TS
private _resolveFirstEmission: () => void;
constructor(innerObservable: Observable<T>, private _timeoutWindow: number, private _suspenseEnabled: boolean) {
super();
this._firstEmission = new Promise<void>((resolve) => (this._resolveFirstEmission = resolve));
this._immutableStatus = {
status: 'loading',
hasEmitted: false,
isComplete: false,
data: undefined,
error: undefined,
firstValuePromise: this._firstEmission
} as ObservableStatus<T>;
this._innerObservable = innerObservable.pipe(
tap({
next: (v) => {
this._next(v);
},
error: (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();
this._updateImmutableStatus();
},
complete: () => {
this._isComplete = true;
this._updateImmutableStatus();
}
}),
catchError(() => empty()),
shareReplay(1)
);
// warm up the observable
this._warmupSubscription = this._innerObservable.subscribe();
// set a timeout for resetting the cache, subscriptions will cancel the timeout
// and reschedule again on unsubscribe
if (this._suspenseEnabled) {
// Noop if suspense is enabled
this._timeoutHandler = setTimeout(() => {}, this._timeoutWindow);
} else {
this._timeoutHandler = setTimeout(this._reset.bind(this), 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;
} else if (!this.hasValue) {
throw Error('Can only get value if SuspenseSubject has a value');
}
return this._value as T;
}
get firstEmission(): Promise<void> {
return this._firstEmission;
}
private _updateImmutableStatus() {
// Cast required because `data` is internally `T | undefined` while a value is
// still in flight, whereas ObservableStatus exposes `data: T` to consumers.
// The relationships between the status fields are verified in tests instead.
this._immutableStatus = {
status: this._error ? 'error' : (this._hasValue ? 'success' : 'loading'),
hasEmitted: this._hasValue,
isComplete: this._isComplete,
data: this._value,
error: this._error,
firstValuePromise: this._firstEmission
} as ObservableStatus<T>;
}
private _next(value: T) {
this._hasValue = true;
this._value = value;
this._resolveFirstEmission();
this._updateImmutableStatus();
}
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<void>((resolve) => (this._resolveFirstEmission = resolve));
this._updateImmutableStatus();
}
_subscribe(subscriber: Subscriber<T>): Subscription {
if (this._timeoutHandler) {
clearTimeout(this._timeoutHandler);
}
this._innerSubscriber = this._innerObservable.subscribe(subscriber);
return this._innerSubscriber;
}
get ourError() {
return this._error;
}
get immutableStatus() {
return this._immutableStatus;
}
}