import { Action, Atom, IObserver, observable, observe } from "../../index" export function isPromiseLike(result: PromiseLike | T): result is PromiseLike { return result && typeof (result as any).then === "function"; } /** * The type returned by the `computedAsync` function. Represents the current `value`. Accessing * the value inside a reaction will automatically listen to it, just like an `observable` or * `computed`. The `busy` property is `true` when the asynchronous function is currently running. */ export interface IComputedAsyncValue { /** The current value (observable) */ readonly value: T; readonly status: { /** True if an async evaluation is in progress */ readonly busy: boolean; /** True if Promise was rejected */ readonly failed: boolean; /** The error from the rejected promise, or undefined */ readonly error: any; } } export interface IComputedAsyncOptions { readonly init: T; readonly fetch: () => PromiseLike | T; readonly delay?: number; readonly revert?: boolean; readonly name?: string; readonly error?: (error: any) => T; readonly rethrow?: boolean; } class ComputedAsync implements IComputedAsyncValue { public status = observable({ busy: false, failed: false, error: null }) private atom: Atom; private cachedValue: T; private version = 0; private monitor: IObserver; constructor(private options: IComputedAsyncOptions) { this.atom = new Atom(() => this.wake(), () => this.sleep()); this.cachedValue = options.init; } private wake() { this.sleep(); this.monitor = observe(() => this.observe(), this.options.delay) } private observe(): void { const thisVersion = ++this.version; if (this.options.revert) { this.cachedValue = this.options.init; this.atom.reportChanged(); } const current = (f: (arg: T) => void) => (arg: T) => { if (this.version === thisVersion) { f(arg) } }; try { const possiblePromise = this.options.fetch(); if (!isPromiseLike(possiblePromise)) { this.stopped(false, undefined, possiblePromise); } else { this.starting(); possiblePromise.then( current((v: T) => this.stopped(false, undefined, v)), current((e: any) => this.handleError(e))); } } catch (x) { this.handleError(x); } } @Action private starting() { this.status.busy = true; } @Action private stopped(f: boolean, e: any, v: T) { this.status.busy = false; this.status.failed = f; this.status.error = e; if (v !== this.cachedValue) { this.cachedValue = v; this.atom.reportChanged(); } } private handleError(e: any) { let newValue = this.options.init; if (this.options.error) { try { newValue = this.options.error(e); } catch (x) { // } } this.stopped(true, e, newValue); } private sleep() { const monitor = this.monitor; this.monitor = undefined; if (monitor) { monitor.unobserve() } } get value() { this.atom.reportObserved(); if (this.status.failed && this.options.rethrow) { throw this.status.error; } return this.cachedValue; } } export function computedAsync( init: T, fetch: () => PromiseLike | T, delay?: number): IComputedAsyncValue; export function computedAsync( options: IComputedAsyncOptions ): IComputedAsyncValue; export function computedAsync( init: T | IComputedAsyncOptions, fetch?: () => PromiseLike | T, delay?: number ) { if (arguments.length === 1) { return new ComputedAsync(init as IComputedAsyncOptions); } return new ComputedAsync({ init: init as T, fetch: fetch!, delay }); }