UNPKG

computed-async-mobx

Version:

Define a computed by returning a Promise

131 lines (102 loc) 3.76 kB
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>; @observable private refreshCallCount: number; @computed 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); } @computed get busy() { const s = this.currentState; return !!(isPromiseBasedObservable(s) && s.state === "pending"); } @action 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; } @computed 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); }