import { observable, runInAction, action } from 'mobx'; import { AsyncReturnType } from './promises'; import { ExtensibleFunction } from './extensibleFunction'; type EFFunc = (...[]) => any; type FuncParamsOption any> = (Parameters) extends [any, ...any[]] ? { params: Parameters } : { params?: Parameters } type LoadState = 'unloaded' | 'loading' | 'loaded' | 'error'; interface EnsureFuncOptions { initialValue?: AsyncReturnType; initialState: LoadState; fetchOnAccess: boolean; } /** * A Class for helping with fetching remote data and tracking status. * When the object is called (either directly or via .ensure()), it executes the function that was given during construction, and return a Promise. * * `state` is transitioned to 'loading' until the function returns, whereon `state` is transitioned to 'loaded' or 'error'. */ export class EnsureFunction extends ExtensibleFunction, Promise>> { constructor(func: F) constructor(options: Partial>, func: F) constructor(a, b?) { super((...args) => this._call(args)); this.func = b || a; const options: EnsureFuncOptions = this.options = { initialState: 'unloaded', fetchOnAccess: true, } Object.assign(options, (b && a) || {}); this._value = options.initialValue; this.state = options.initialState; } private options: EnsureFuncOptions private func: F; currentPromise: Promise> @observable accessor _value: AsyncReturnType; @observable accessor error; @observable accessor state: LoadState = 'unloaded'; async _call(args: Parameters) { if (this.state == 'loaded') return this._value; if (this.state == 'loading') return await this.currentPromise; runInAction(() => this.state = 'loading'); try { const value = await this.func(...args); this.setValue(value); return value; } catch (e) { runInAction(() => { this.error = e; this.state = 'error' }); throw e; } } get value() { if (this.options.fetchOnAccess && this.state == 'unloaded') { // @ts-ignore this.ensure(); } return this._value; } @action markLoaded() { this.error = null; this.state = 'loaded'; } @action setValue(value: AsyncReturnType) { this._value = value; this.error = null; this.state = 'loaded'; } async ensure(...args: Parameters) { return this._call(args); } async reload(...args: Parameters) { this.reset(true); return this.ensure(...args); } @action reset(keepValue = false) { this.state = 'unloaded'; if (!keepValue) { this._value = null; this.error = null; } } }