/// namespace Leaf { interface FeedInfo { feedListener?: { (): void } signals: TValue[] name: TKey } type Feeds = { [K in keyof TSignalEnum]?: FeedInfo } export type TStateDefaultEnum = "void" | "panic" | "recursivePanic" export class States{ public events = new Leaf.EventEmitter<{ reset?: void state?: TStateEnum | TStateDefaultEnum [key: string]: any }>() public state: TStateEnum | TStateDefaultEnum = "void" public previousState: TStateEnum | TStateDefaultEnum private sole: number = 1 private soleEmitted: number = 1 private lastException: Error = null private rescues = [] protected data: any = {} private feeds: Feeds = {} as Feeds private isDebugging: boolean = false private clearHandler: { (): void } = null public name: string = this.constructor && this.constructor["name"] || null public panicErrorRecursive: Error = null public panicError: Error = null public panicState: TStateEnum | TStateDefaultEnum = null constructor() { this.reset() } public reset() { this.data = {} this.feeds = {} this.respawn() this.events.emit("reset") } public errorOrGoto(err, state: string, ...args: any[]) { if (err) { this.error(err) return } this.setState(state as any, ...args) } public setState(state: TStateEnum | TStateDefaultEnum, ...args: any[]): void { try { this._setState(state, ...args) } catch (e) { console.error(e) if (state == "panic") { this.setState("recursivePanic") } else { this.error(e) } } } private _stateFinish() { try { this.events.emit("state", this.state) this.events.emit("state/" + this.state) } catch (e) { console.error(e) } this.soleEmitted = this.sole this._updateStateAssertion(this.state as TStateEnum) } protected clearConsumers() { if (this.feeds) { for (let prop in this.feeds) { let feed = this.feeds[prop] feed.feedListener = null } } } private _setState(state: TStateEnum | TStateDefaultEnum, ...args: any[]) { if (this.state === "panic" && state !== "void") { return } // set state syncly from previous state // so we emit the previous state here // That is, all state/${state} is guaranteed to be emitted // after the state finish and before the next state start. this.clear() if (this.soleEmitted < this.sole) { this._stateFinish() } // clear all consumers this.clearConsumers() // new sole this.sole += 1 this.previousState = this.state this.state = state if (this.isDebugging) { this.debugStateHandler() } let stateHandler = "at" + state[0].toUpperCase() + (state as string).substring(1) if (this[stateHandler]) { let sole = this.sole try { this[stateHandler](function () { return sole !== this.sole }.bind(this), ...args) } catch (e) { if (this.state == "panic") { console.error(e) console.error("Recursive panic") this.panicErrorRecursive = e this.setState("recursivePanic") return } this.error(e) return } // didn't set state during the process, we can safely emit state here if (sole === this.sole) { this._stateFinish() } } else if (state !== "void") { this.error(new Error(`InvalidState ${state}`)) } } public error(err: Error) { this.panicError = err this.panicState = this.state this._logger(`Panic at ${this.state}`, this.panicError) if (this.panicError) { this.setState("panic") } } protected atPanic() { console.error("Panic at", this.panicState, this.panicError) this.events.emit("panic", this.panicError, this.panicState) } public recover(recoverState?: TStateEnum | TStateDefaultEnum) { let error = this.panicError let state = this.panicState this.respawn() if (!recoverState) { recoverState = ("void") } this.setState(recoverState) return { error, state } } public respawn() { this.sole = this.sole || 1 this.sole += 1 this.soleEmitted = this.sole this.panicError = null this.panicState = null this.setState("void") this.clear() } public clear(handler?: { (): void }) { if (handler) { if (this.clearHandler) { throw new Error("Already clearing with a handler") } this.clearHandler = handler } else { handler = this.clearHandler this.clearHandler = null if (handler) { handler() } } } private ensureFeed(name: K): FeedInfo { if (this.feeds[name]) return this.feeds[name]; let feed = this.feeds[name] = { feedListener: null, signals: [], name: name } return feed } public feed(name: keyof TSignalEnum, signal?: any) { if (!this.feeds[name]) { this.feeds[name] = { feedListener: null, signals: [], name: name } } let feed = this.feeds[name] feed.signals.push(signal) let listener = feed.feedListener if (listener) { feed.feedListener = null try { listener() } catch (e) { this.error(e) } } } public consumeAll(name: keyof TSignalEnum): number { let feed = this.feeds[name] if (feed) { let length = feed.signals.length feed.signals.length = 0 return length } return 0 } public hasFeed(name: keyof TSignalEnum): boolean { return this.feeds[name] && this.feeds[name].signals.length > 0 } public consume(name: keyof TSignalEnum): boolean | any { if (!this.hasFeed(name)) return null let feed = this.feeds[name] if (feed) { return feed.signals.shift() || true } return null } public consumeWhenAvailableMergeToLast(name: K, callback: { (signal: TSignalEnum[K]): void }) { this.consumeWhenAvailable(name, (detail) => { let last = null while (true) { last = this.consume(name) if (!last) break; } try { if (last) { callback(last) } else { callback(detail) } } catch (e) { this.error(e) } }) } public isConsuming(name: keyof TSignalEnum): boolean { return this.feeds[name] && this.feeds[name].feedListener && true || false } public consumeWhenAvailable(name: keyof TSignalEnum, callback: { (signal: any): void }) { let feed = this.feeds[name] if (!feed) { feed = this.ensureFeed(name) } let called = false feed.feedListener = () => { if (called) return called = true callback(this.consume(name)) } this.setImmediate(() => { let feed = this.feeds[name] // cleared by others if (!feed || !feed.feedListener) return // have signals if (feed && feed.signals && feed.signals.length > 0) { try { let listener = feed.feedListener listener() } catch (e) { this.error(e) } } this.events.emit("starve", name) this.events.emit("starve/" + name) }) } public debug(option?: any) { this.isDebugging = true } private debugStateHandler(option?: { name?: string }, message?: string) { option = option || {} let name = option.name || this.name || "Anonymous" let text = `${name}: state - ${this.state}` this._logger(text) } public setLogger(logger: (...infos: any[]) => void) { this._logger = logger } private _logger: (...infos: any[]) => void = (...infos: any[]) => { let log = console.log log.apply(console, infos) } private _assertingStateSequence: TStateEnum[] = null private _assertingStateCallback: Function = null private _assertingStateIndex = 0 public assertStateSequence(states: TStateEnum[], callback: (err: Error) => void) { if (this._assertingStateSequence) { callback(new Error("Already asserting")) return } this._assertingStateSequence = states this._assertingStateCallback = callback this._assertingStateIndex = 0 } private _endStateAssertion(err?: Error) { let cb = this._assertingStateCallback if (!cb) return this._assertingStateSequence = null this._assertingStateIndex = 0 this._assertingStateCallback = null cb(err) } private _updateStateAssertion(state: TStateEnum) { if (!this._assertingStateSequence) return let should = this._assertingStateSequence[this._assertingStateIndex] if (should !== state) { this._endStateAssertion(new Error(`State at index ${this._assertingStateIndex} should be ${should} but is ${state} during sequence ${this._assertingStateSequence.map((s, index: number) => { if (index === this._assertingStateIndex) return "->" + s; return s; }).join("\n")}`)) } } protected atRecursivePanic() { this._logger("RecursivePanics", this.panicError) } private setImmediate(fn) { if (typeof setImmediate !== "undefined") { setImmediate(fn) } else { setTimeout(fn, 0) } } } }