logicoma
Version:
A human FSM(friendly state machine) with good async support and error handling
350 lines (345 loc) • 11.6 kB
text/coffeescript
# States
#
# How it works
# 1. states should start from "void", there should be no atVoid handler.
# 2. by calling @error current state and error will be saved
# and we will turn the state into "panic".
# 3. at "panic" states, state machine won't turn unless you call @recover()
# or setState to "void" manually.
# Latter one won't reset @panicError and @panicState.
# 4. you should do the error recovering in atPanic handler, or just emit "panic"
# event to the parent.
# 5. all the local data should be store on @data, so we can easily recover
# from the previous shutdown.
# 6. should be rebust against invalid states jump
# 7. we have a default atPanit state handler to just emit a "panic" event
# so the parent of this state machine should come to rescue
# but in case we know how to recover from the current state, we may over
# write atPanic handler to suppress the panic event
# Note: Set state with same stateName of the current state again will do nothing.
#
#
# States using a unique Sole to prevent multiple running context
# at async action.
#
# atFetching:(sole)->
# asyncFetchin ()=>
# # check soles to prevent multiple runing context
# if not @checkSole sole
# return
#
if typeof Leaf isnt "undefined"
EventEmitter = Leaf.EventEmitter
Errors = Leaf.ErrorDoc.create()
.define("AlreadyDestroyed")
.define("InvalidState")
.generate()
else
EventEmitter = (require "eventex").EventEmitter
Errors = (require "error-doc").create()
.define("AlreadyDestroyed")
.define("InvalidState")
.generate()
class States extends EventEmitter
@Errors = Errors
constructor:()->
@state = "void"
@_sole = 1
@_soleEmitted = 1
@lastException = null
@states = {}
@rescues = []
@data = {}
@forceAsync ?= false
# @_listenBys = []
if @_isDebugging
@debug()
super()
declare:(states...)->
for state in states
@states[state] = state
destroy:()->
if @isDestroyed
return;
@emit "destroy"
@isDestroyed = true
@emit = ()->
@on = ()->
@once = ()->
@removeAllListeners()
extract:(fields...)->
data = {}
for item in fields
data[item] = @data[item]
return data
setData:(data)->
for prop of data
if data.hasOwnProperty prop
@data[prop] = data[prop]
at:(state,callback)->
handlerName = "at"+state[0].toUpperCase()+state.substring(1)
this[handlerName] = callback
return this
_nextTick:(exec)->
if typeof setImmediate isnt "undefined"
fn = setImmediate
else
fn = (exec)=>
timer = setTimeout ()=>
exec()
,4
return timer
return fn(exec)
_clearTick:(timer)->
if typeof setImmediate isnt "undefined"
fn = clearImmediate
else
fn = clearTimeout
setState:(state,args...)->
@_clearTick @_stateTimer
if @forceAsync
@_stateTimer = @_nextTick ()=>
if @_isDebugging
@_setState state,args...
else
@_try ()=>
@_setState state,args...
else
if @_isDebugging
@_setState state,args...
else
@_try ()=>
@_setState state,args...
_try:(fn)=>
try
fn()
catch e
@error e
_setState:(state,args...)->
@_clearTick @_stateTimer
if not state
throw new Errors.InvalidState "Can't set invalid states #{state}"
if @state is "panic" and state isnt "void"
return
if @isDestroyed
return
if @data.feeds
for prop,item of @data.feeds
item.feedListener = null
if @_soleEmitted < @_sole
@emit "state",state
@emit "state/#{state}"
@_soleEmitted = @_sole
@_sole += 1
@stopWaiting()
@previousState = @state
@state = state
if @_isDebugging and @_debugStateHandler
@_debugStateHandler()
stateHandler = "at"+state[0].toUpperCase()+state.substring(1)
if this[stateHandler]
sole = @_sole
this[stateHandler] ()=>
sole isnt @_sole
,args...
# didn't set state in the progress
console.error "SM"
if sole is @_sole
@_soleEmitted = sole
@emit "state",state
@emit "state/#{state}"
else if state not in ["void"]
if console.warn
console.warn "state handler #{stateHandler} not provided"
else
console.error "state handler #{stateHandler} not provided"
error:(error)->
@panicError = error
@panicState = @state
for rescue in @rescues
if rescue.state is @panicState and (@panicError instanceof rescue.error or not rescue.error)
if @_debugRescueHandler
@_debugRescueHandler()
@recover()
rescue.callback(error)
break
# does rescue handles all error
if @panicError
@setState "panic"
recover:(recoverState)->
# For safety, recover just do a respawn.
# So every async call should be ignored,
# only if they forgot to check sole.
error = @panicError
state = @panicState
@respawn()
if recoverState
@setState recoverState
return {error,state}
rescue:(state,error,callback = ()->)->
if not callback
throw new Error "rescue should provide callbacks"
@rescues.push {state,error,callback}
give:(name,items...)->
if @_waitingGiveName is name
handler = @_waitingGiveHandler
@_waitingGiveName = null
@_waitingGiveHandler = null
if @_isDebugging and @_debugRecieveHandler
@_debugRecieveHandler(name,items...)
handler.apply this,items
return
stopWaiting:(name)->
if name
if @_waitingGiveName is name
@_waitingGiveName = null
@_waitingGiveHandler = null
else
throw new Error "not waiting for #{name}"
else
@_waitingGiveName = null
@_waitingGiveHandler = null
isWaitingFor:(name)->
if not name and @_waitingGiveName
return true
if name is @_waitingGiveName
return true
return false
feed:(name,item = null)->
@data.feeds ?= {}
@data.feeds[name] ?= []
@data.feeds[name].push(item)
if listener = @data.feeds[name].feedListener
@data.feeds[name].feedListener = null
listener()
consumeAll:(name)->
if @data.feeds?[name]?
length = @data.feeds[name].length or 0
@data.feeds[name] = []
return length
return 0
hasFeed:(name)->
return @data.feeds?[name]?.length > 0
consume:(name)->
if not @hasFeed name
return null
if @data.feeds?[name]?
return @data.feeds[name].shift() or true
consumeWhenAvailableMergeToLast:(name,callback)->
@consumeWhenAvailable name,(detail)=>
while last = @consume name
continue
if last
callback(last)
else
callback detail
consumeWhenAvailable:(name,callback)->
@data.feeds ?= {}
@data.feeds[name] ?= []
if @data.feeds[name].length > 0
callback @consume(name)
else
@data.feeds[name].feedListener = ()=>
callback @consume(name)
#console.error "startve"
@emit "starve",name
@emit "starve/#{name}"
waitFor:(name,handler)->
if @_waitingGiveName
throw new Error "already waiting for #{@_waitingGiveName} and can't wait for #{name} now"
@_waitingGiveName = name
@_waitingGiveHandler = handler
if @_isDebugging and @_debugWaitHandler
@_debugWaitHandler()
@emit "wait",name
@emit "wait/#{name}"
atPanic:()->
if @_isDebugging and @_debugPanicHandler
@_debugPanicHandler()
console.error @panicError,@panicState
@emit "panic",@panicError,@panicState
reset:(data = {})->
@data = data
@respawn()
@emit "reset"
getSole:()->
return @_sole
checkSole:(sole)->
return @_sole is sole
stale:(sole)->
if typeof sole is "function"
return sole()
return @_sole isnt sole
respawn:()->
@_sole = @_sole or 1
@_sole += 1
@_waitingGiveName = null
@_waitingGiveHandler = null
@panicError = null
@panicState = null
@setState "void"
@_clearTick @_stateTimer
@clear()
# listenBy:(who,event,callback)->
# owner = null
# for item in @_listenBys
# if item.who is who
# owner = item
# break
# if not owner
# owner = {who:who,cases:[]}
# @_listenBys.push owner
# owner.cases.push {event:event,callback:callback}
# @on event,callback
# stopListenBy:(who,event)->
# owner = null
# for item in @_listenBys
# if item.who is who
# owner = item
# break
# if not owner
# return
# for item,index in owner.cases
# if item and (item.event is event or not event)
# @removeListener item.event,item.callback
# owner.cases[index] = null
# owner.cases = owner.cases.filter (item)->item
# if owner.cases.length is 0
# @_listenBys = @_listenBys.filter (item)->item isnt owner
debug:(option = {})->
close = option.close
@_debugName = option.name or @constructor and @constructor.name or "Anonymouse"
_console = option.console or console
log = ()->
if _console.debug
_console.debug.apply _console,arguments
else
_console.log.apply _console,arguments
if close
@_isDebugging = false
else
@_isDebugging = true
@_debugStateHandler ?= ()=>
log "#{@_debugName or ''} state: #{@state}"
@_debugWaitHandler ?= ()=>
log "#{@_debugName or ''} waiting: #{@_waitingGiveName}"
@_debugRescueHandler ?= ()=>
log "#{@_debugName or ''} rescue: #{@panicState} => #{@panicError}"
@_debugPanicHandler ?= ()=>
log "#{@_debugName or ''} panic: #{JSON.stringify @panicError}"
@_debugRecieveHandler ?= (name,data...)=>
log "#{@_debugName or ''} recieve: #{name} => #{data.join(" ")}"
clear:(handler)->
if handler
if @_clearHandler
throw new Error "already has clear handler"
@_clearHandler = handler
else
_handler = @_clearHandler
@_clearHandler = null
if _handler
_handler()
if typeof Leaf isnt "undefined"
Leaf.States = States
else
module.exports = States