import {HyperValue, recordAsync, PromiseWrapper} from '../core'; import {BaseScope} from './base'; interface Dep { watcherId: number; hvId: number; } export interface HvAsyncParams { initial?: I; get?: AsyncGetter; set?: AsyncSetterApprove; update?: AsyncSetter; } export class HvAsync extends HyperValue { state = new HyperValue('resolved') as HyperValue<'pending' | 'resolved' | 'rejected'>; private getter: AsyncGetter | undefined; private setter: AsyncSetter | undefined; private hs: BaseScope; private callId = 0; private currentPromise: Promise; private resolver: () => void; private rejecter: (error: Error) => void; constructor(hs: BaseScope, params: HvAsyncParams) { super(params.initial as I); this.hs = hs; this.getter = params.get; if (params.set && params.update) { throw new Error('both set and update cannot be defined'); } if (params.update) { this.setter = params.update; } if (params.set) { this.setter = value => { const setter = params.set as AsyncSetterApprove; return setter(value).then(() => value); }; } this.initPromise(); this.init(); } private initPromise() { this.currentPromise = new Promise((resolve, reject) => { this.resolver = resolve; this.rejecter = reject; }); } private fetch(fn: () => Promise): Promise { return new Promise((resolve, reject) => { this.callId++; const id = this.callId; this.state.$ = 'pending'; fn().then( value => { if (this.callId === id) { super.s(value); this.state.$ = 'resolved'; this.resolver(); this.initPromise(); } resolve(value); }, error => { if (this.callId === id) { this.state.$ = 'rejected'; this.rejecter(error); this.initPromise(); this.hs.fail(this, error); } reject(error); } ); }); } wait(): Promise> { return this.currentPromise.then(() => this as any as HvAsync); } private init() { if (!this.getter) { return; } let depList = [] as Dep[]; const watchDeps = (hvIdList: number[]) => { return hvIdList.map(hvId => { return { hvId, watcherId: this.hs.watch(hvId, watcher) }; }); }; const watcher = () => { for (const dep of depList) { this.hs.unwatch(dep.hvId, dep.watcherId); } recordAsync(w => { const getter = this.getter as AsyncGetter; return this.fetch(() => getter(w)); }, deps => { depList = depList.concat(watchDeps(deps.map(hv => hv.id))); }) .catch(() => { depList = watchDeps(depList.map(dep => dep.hvId)); }); }; watcher(); } s(newValue: T) { if (!this.setter) { super.s(newValue); return; } this.fetch(() => { return (this.setter as AsyncSetter)(newValue); }).then(value => { super.s(value); }); } } export interface AsyncGetter { (w: PromiseWrapper): Promise; } export interface AsyncSetter { (value: T): Promise; } export interface AsyncSetterApprove { (value: T): Promise; } export class AsyncScope extends BaseScope { async(params: HvAsyncParams): HvAsync { return new HvAsync(this, params); } }