computed-async-mobx
Version:
Define a computed by returning a Promise
131 lines (102 loc) • 3.76 kB
text/typescript
import { computed, action, observable, runInAction, autorun } from "mobx"
import { getGlobalState } from "./mobxShim";
import { fromPromise, IPromiseBasedObservable, isPromiseBasedObservable } from "mobx-utils";
import { Getter } from "./Getter";
export function isPromiseLike<T>(result: PromiseLike<T>|T): result is PromiseLike<T> {
return result && typeof (result as any).then === "function";
}
/**
* PromisedComputedValue
*/
export interface PromisedComputedValue<T> extends Getter<T> {
/** True if the promise is currently resolving */
readonly busy: boolean;
refresh(): void;
getNonReactive(): T;
}
type PromiseResult<T> = { ok: true; value: T } | { ok: false; error: any };
function value<T>(value: T): PromiseResult<T> {
return { ok: true, value };
}
function error<T>(error: any): PromiseResult<T> {
return { ok: false, error };
}
class PromisedComputed<T> implements PromisedComputedValue<T> {
private cached: PromiseResult<T>;
private refreshCallCount: number;
private get currentState(): IPromiseBasedObservable<PromiseResult<T>> | PromiseResult<T> {
try {
this.refreshCallCount;
const promiseOrValue = this.fetch();
return isPromiseLike(promiseOrValue)
? fromPromise(promiseOrValue.then(value, e => error<T>(e)))
: value(promiseOrValue);
} catch (x) {
return error<T>(x);
}
}
constructor(init: T,
private readonly fetch: () => PromiseLike<T> | T,
private disableReactionChecking?: boolean) {
runInAction(() => this.refreshCallCount = 0);
this.cached = value(init);
}
get busy() {
const s = this.currentState;
return !!(isPromiseBasedObservable(s) && s.state === "pending");
}
refresh() {
this.refreshCallCount++;
}
get() {
if (!this.disableReactionChecking &&
!getGlobalState().trackingDerivation) {
throw new Error("promisedComputed must be used inside reactions");
}
return this.value;
}
/**
* This exists purely to support scenarios such as unit tests that
* want to verify the most recent value outside of a reactive context
*/
getNonReactive() {
let result: T = undefined!;
autorun(() => result = this.get())();
return result;
}
private get value(): T {
const s = this.currentState;
const r = !isPromiseBasedObservable(s) ? s :
s.state === "fulfilled" ? s.value :
this.cached;
this.cached = r;
if (r.ok) {
return r.value;
}
throw r.error;
}
}
/**
* Similar to the standard computed, except that it converts promises into
* plain values, unwrapping them when they resolve and updating to the new
* value. The supplied function may return a plain value in which case the
* update is entirely synchronous like standard computed.
*
* As with the standard computed, exceptions (and rejected promises) are
* propagated as re-thrown exceptions. To avoid this, perform your own
* error handling in your supplied function.
*
* @param init Value to assume until the promise first resolves
* @param compute Evaluates to a promised or plain value
*/
export function promisedComputed<T>(init: T, compute: () => PromiseLike<T> | T): PromisedComputedValue<T> {
return new PromisedComputed<T>(init, compute);
}
export function promisedComputedInternal<T>(init: T, compute: () => PromiseLike<T> | T): PromisedComputedValue<T> {
return new PromisedComputed<T>(init, compute, true);
}