///
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)
}
}
}
}