O = require 'omicron'
state = require './state-function'
State = require './state'
StateExpression = null
Transition = null
TransitionExpression = null
module.exports =RootState
A root state is the top-level State which holds the authoritative
reference to the owner’s current state, and which governs the proceeding
of transitions that traverse its state tree.
class RootState extends State
{ rxTransitionArrow, transitionArrowMethods } = state
{ env, hasOwn, trim, type, isEmpty, isArray } = O
{ slice } = Array::
{ VIRTUAL, ABSTRACT, CONCLUSIVE, FINAL } = this
{ VIA_NONE, VIA_PROTO } = thisConstructor
Creates a working state tree based on a single root state as directed
by an expression, and implements this tree as a behavior model for the
provided owner.
PARAMETERS
owner: objectexpression:StateExpression| object — A plain object will be coerced and interpreted if necessary into a formalStateExpression.accessorName: string — The property name onownerat which the generated accessor function will appear. Defaults to'state'.initialState: string — A state name or path. If present, supersedes theinitialattribute of substates or inherited protostates.
DISCUSSION
Direct construction via new is for internal use only; the State object
model is properly created by defining StateExpressions that are provided to
the exported state function.
SOURCE
constructor: ( owner = {}, expression, accessorName, initialState ) ->Assign to owner an accessor to its state implementation.
@accessorName = accessorName ?= 'state'
owner[ accessorName ] = createAccessor owner, accessorName, thisA root state’s name by definition is the empty-string.
super owner, '', expressionDetermine the initial state, and set the current state to that.
current = ( if initialState? then @query initialState else
@initialSubstate() ) ? thisThe initial state may be abstract, in which case the current reference must
snap to the abstract state’s default concrete substate.
if current.attributes & ABSTRACT
current = current.defaultSubstate() ? currentThe previous redirections may have left current pointing to a protostate, in
which case a virtual state must be created in this root’s tree.
current = current.virtualize this if current.root isnt thisWith current now resolved, the authoritative reference to it is held here.
@_current = currentWhile a Transition is underway, _current will be a reference to that
transition. To further distinguish this condition, an identical reference is
also held at _transition.
@_transition = nullPrivate functions
createAccessor
Returns the accessor function that will serve as an owner object’s interface
to its state implementation.
createAccessor = ( owner, name, root ) ->
accessor = ( input, args... ) ->
current = root._current or root
if this is owner
return current unless input?
return current.change input.call this if typeof input is 'function'
if typeof input is 'string' and
( match = input.match rxTransitionArrow ) and
method = transitionArrowMethods[ match[1] ]
return if args.length
then current[ method ].apply current, [ match[2] ].concat args
else current[ method ] match[2]
return current.query.apply current, argumentsCalling the accessor of a prototype means that this requires its own accessor
and root state. Creating a new RootState will have the desired side-effect of
also creating the object’s new accessor, to which the call is then forwarded.
else if ( owner.isPrototypeOf this ) and
( ( not hasOwn.call this, name ) or @[ name ] is owner[ name ] )
new RootState this, null, name, current.path()
return @[ name ].apply this, arguments
accessor.isAccessor = true
if env.debug
accessor.toString = ->
"[accessor] -> #{ root._current.path() }"
accessorevaluateGuard
Returns the boolean result of a guard function in the context of a State,
as evaluated against another State. Defaults to true if no guard exists.
evaluateGuard = ( context, guard, against ) ->
guard = context.guard guard if typeof guard is 'string'
return true unless guard
args = slice.call arguments, 1
for own key, value of guard
valueIsFn = typeof value is 'function'
selectors = trim( key ).split /\s*,+\s*/
for selector in selectors when context.query selector, against
result = if valueIsFn then value.apply context, args else value
break
break unless result
!!resultMethods
getTransitionExpression
Finds the appropriate transition expression for the given target and origin
states. If no matching transitions are defined in either state or any of their
ancestors, a generic actionless transition expression for the pair is returned.
PARAMETERS
target:Stateorigin:State(optional) — defaults to thecurrentstate.
SOURCE
getTransitionExpression: do ->
search = ( target, origin, subject, ceiling ) ->
while subject and subject isnt ceiling
for own key, expr of subject.transitions()
return expr if (
not ( guards = expr.guards ) or (
not ( admit = guards.admit ) or
isEmpty( admit ) or
evaluateGuard.call origin, admit, target, origin
) and (
not ( release = guards.release ) or
isEmpty( release ) or
evaluateGuard.call target, release, origin, target
)
) and (
if expr.target then subject.query expr.target, target
else subject is target
) and ( not expr.origin or subject.query expr.origin, origin )
break unless ceiling?
subject = subject.superstate
( target, origin = @_current ) ->
( search target, origin, target ) or
( search target, origin, origin unless origin is target ) or
( search target, origin, target.superstate, @root ) or
( search target, origin, @root ) or
( unless target.isIn origin
search target, origin, origin.superstate, origin.common target ) or
new TransitionExpressionchange
Attempts to execute a state transition.
SYNOPSIS
A change/go/be operation conducts asynchronous transitions, generation of
relevant transitional events, and construction of any necessary temporary
virtual states for prototypal inheritors.
Rules imposed by all guards held on both the origin and target states are
respected, and if these are not satisfied the transition will be denied.
PARAMETERS
The target parameter may be either a State object within the tree of this
RootState, or a string that resolves to a likewise targetable State when
evaluated from the context of the most recently current state.
The options parameter is an optional map that may include:
args: array — arguments to be passed to a transition’saction.success: function — invoked if the transition attempt succeeds.failure: function — invoked if the transition attempt fails.
SOURCE
change: ( target, options ) ->
{ root, owner } = this
current = @_current
transition = @_transitionThe origin is defined as the controller’s most recently current state that is
not a Transition.
origin = transition?.origin or currentDepartures are not allowed from a state that is final.
return null if origin.attributes & FINALEnsure that target is a valid State.
unless target instanceof State
target = if target then origin.query target else root
return null unless target
targetOwner = target.owner
return null if owner isnt targetOwner and
not targetOwner.isPrototypeOf ownerExtract args from options and resolve options to an object if necessary.
if options?
args = if ( isArray options ) or ( type options ) is 'arguments'
then options
else options.args
args = slice.call args if args?A transition cannot target an abstract state directly, so target must be
reassigned to the appropriate concrete substate.
while target.attributes & ABSTRACT
return null unless target = target.defaultSubstate()If any guards are in place for the given origin and target, both of those
states must consent to the transition.
unless options?.forced
released = evaluateGuard origin, 'release', target
admitted = evaluateGuard target, 'admit', origin
unless released and admitted
options?.failure?.call? this
return nullIf target is a protostate, then the protostate must be virtualized locally
and target must be reassigned to the new virtual state.
target = target.virtualize this unless target?.root is rootA reference is held to the previously current state, or abortive transition.
source = currentThe upcoming transition will start from its source and proceed within the
domain of the least common ancestor between source and target.
domain = source.common targetConclusivity is enforced by checking each state that will be exited against the
conclusive attributes.
s = source; until s is domain
return null if s.attributes & CONCLUSIVE
s = s.superstateIf a previously initiated transition is still underway, it needs to be notified that it will not finish.
transition?.abort()Retrieve the appropriate transition expression for this origin/target pairing. If none is defined, then an actionless default transition will be created and applied, causing the callback to return immediately.
@_transition = transition = new Transition target, source,
@getTransitionExpression target, originPreparation for the transition begins by emitting a depart event on the
source state.
eventArgs = [ transition, args ]
source.emit 'depart', eventArgs, VIA_PROTO
@_transition = transition = null if transition.abortedEnter into the transition state.
if transition
@_current = transition
transition.emit 'enter', VIA_NONE
@_transition = transition = null if transition.abortedWalk up to the top of the domain, emitting exit events for each state along
the way.
s = source; while transition and s isnt domain
s.emit 'exit', eventArgs, VIA_PROTO
transition.superstate = s = s.superstate
@_transition = transition = null if transition.abortedA callback will be invoked from transition.end() to conclude the transition.
transition?.callback = ->
transition = null if transition.abortedTrace a path from target up to domain, then walk down it, emitting enter
events for each state along the way.
if transition
s = target; pathToState = []; while s isnt domain
pathToState.push s
s = s.superstate
s = domain; while transition and substate = pathToState.pop()
transition.superstate = substate
substate.emit 'enter', eventArgs, VIA_PROTO
transition = null if transition.aborted
s = substateExit from the transition state.
if transition
transition.emit 'exit', VIA_NONE
transition = null if transition.abortedTerminate the transition with an arrive event on the targeted state.
if transition
@_current = target
target.emit 'arrive', eventArgs, VIA_PROTOAny virtual states that were previously active may now be discarded.
s = origin; while s.attributes & VIRTUAL
ss = s.superstate
do s.destroy
s = ssNow complete, the Transition instance can be discarded.
do transition.destroy
@_transition = transition = null
options?.success?.call? this
return target
return nullAt this point the transition is attached to the domain state and is ready to
proceed.
return transition?.start.apply( transition, args ) or @_currentStateExpression = require './state-expression'
Transition = require './transition'
TransitionExpression = require './transition-expression'