import {scopedObservable} from "./angular-rx-utils"; import Observable = Rx.Observable; import IScope = angular.IScope; import IPromise = Rx.IPromise; export abstract class StoreElement { public pathInStore: string = null; public logFn: (msg: any) => any = null; log(msg: string, reason?: string) { reason = reason === undefined ? "" : " // " + reason; if (this.pathInStore && this.logFn) { this.logFn("[" + this.pathInStore + "] " + msg + reason); } } } interface PromiseLike { then(successCallback: (value: T) => any, errorCallback: (value: T) => any): any; } export class State extends StoreElement { private timestampOfLastValue = -1; private timestampOfLastPromise = -1; private subject = new Rx.BehaviorSubject(null); private lastValue: T = null; private cleared = new Rx.Subject(); private observable: Observable; constructor() { super(); this.observable = this.subject.filter(val => val !== null && val !== undefined); } /** * Returns true if this state either has a value of if * a value is awaited from a promise (via putFromPromise). */ public isPristine(): boolean { return this.timestampOfLastValue === -1 && this.timestampOfLastPromise === -1; } public isValueOrPromiseOlderThan(timeoutInMs: number) { const ageValue = Date.now() - this.timestampOfLastValue; const agePromise = Date.now() - this.timestampOfLastPromise; return ageValue > timeoutInMs && agePromise > timeoutInMs; } public hasValue(): boolean { return this.lastValue !== null && this.lastValue !== undefined; } /** * Returns the current value or 'null', if no value is present. * Therefore, calls to this method should always be guarded by State#hasValue(). * * However, it is usually better to use State#get()/State#observe(). */ public getCurrentValue(): T { return this.lastValue; } public clear(reason?: string): this { this.log("State#clear()", reason); this.setState(null); return this; } public put(value: T, reason?: string): this { this.log("State#put(...)", reason); this.setState(value); return this; } public putFromPromise(promise: PromiseLike): this { this.clear(); this.timestampOfLastPromise = Date.now(); promise.then( // success (value: T) => { this.log("State#putFromPromise(...)"); this.setState(value); }, // error () => { this.log("State#putFromPromise ERROR"); this.timestampOfLastPromise = -1; } ); return this; } public putFromPromiseIfPristine(calledIfPristine: () => PromiseLike): this { if (this.isPristine()) { this.putFromPromise(calledIfPristine()); } return this; } public get(): IPromise { return this.observable.take(1).toPromise(); } public observe(scope: IScope): Observable { return this.scopedObservable(scope); } public observeCleared(scope: IScope): Observable { return scope ? scopedObservable(scope, this.cleared.asObservable()) : this.cleared.asObservable(); } private setState(val: T) { this.lastValue = val; this.subject.onNext(val); if (val === null || val === undefined) { this.timestampOfLastValue = -1; this.timestampOfLastPromise = -1; this.cleared.onNext(null); } else { this.timestampOfLastValue = Date.now(); } } private scopedObservable(scope: IScope): Observable { return scope ? scopedObservable(scope, this.observable) : this.observable; } } export class MultiState extends StoreElement { private states: {[id: string]: State} = {}; constructor() { super(); } clearAll() { this.states = {}; } put(id: string, value: T): State { this.log("MultiState#put(" + id + ")"); const state = this.get(id); state.put(value); return state; } get(id: string): State { if (this.states[id] === undefined) { this.states[id] = new State(); } return this.states[id]; } } function traverse(elem: any, path: string, logFn: (msg: any) => any) { for (const key in elem) { if (!elem.hasOwnProperty(key)) { continue; } const value = elem[key]; let location = path.length > 0 ? path + "." + key : key; if (value instanceof StoreElement) { value.pathInStore = location; value.logFn = logFn; } else { traverse(value, location, logFn); } } } export function initStates(states: any, logFn?: (msg: any) => any) { return traverse(states, "", logFn); }