O = require 'omicron'
state = require './state-function'
StateEventEmitter = null
StateExpression = null
TransitionExpression = null
{ STATE_ATTRIBUTES, TRAVERSAL_FLAGS } = state
module.exports =State
A state defines a subset of behavior for its owner object. Each
State holds a reference to a superstate, from which it may
inherit more generic behavior, forming a state tree rooted by a single
RootState.
The owner’s RootState designates exactly one of the States in its tree as
its current state. This reference may be transitioned to a different
State in the tree, causing the owner’s projected behavior to change.
An owner and its tree of States are also heritable along the owner’s
prototype chain. Inheritors of a stateful prototype effectively possess all of
the prototype’s States, but each inheritor can adopt its own current state
and instigate transitions independently of the prototype.
class State
{ memoizeProtostates, useDispatchTables } = state.options
{ env, NIL, isArray, isEmpty, has, hasOwn } = O
{ assign, edit, delta, clone, lookup, flatten } = OBit field constants will be used extensively throughout the class’s constructor and methods, so make them available as free variables.
{
INCIPIENT, ATOMIC, DESTROYED
VIRTUAL
PARASTATIC
MUTABLE, FINITE, STATIC, IMMUTABLE
INITIAL, CONCLUSIVE, FINAL
ABSTRACT, CONCRETE, DEFAULT
REFLECTIVE
HISTORY, RETAINED, SHALLOW
CONCURRENT
NORMAL
} =
assign this, STATE_ATTRIBUTESFor methods that query related states, the default behavior is to recurse through substates, superstates, and protostates.
{ VIA_NONE, VIA_SUB, VIA_SUPER, VIA_PROTO, VIA_ALL } =
assign this, TRAVERSAL_FLAGSPrecompute certain useful attribute combinations.
MUTABLE_OR_FINITE = MUTABLE | FINITE
ABSTRACT_OR_CONCRETE = ABSTRACT | CONCRETE
INCIPIENT_OR_VIRTUAL = INCIPIENT | VIRTUAL
INCIPIENT_OR_MUTABLE = INCIPIENT | MUTABLEA bit mask indicates the attributes that can be inherited via protostates.
PROTO_HERITABLE_ATTRIBUTES =
PARASTATIC |
MUTABLE | FINITE | STATIC | IMMUTABLE |
INITIAL | CONCLUSIVE | FINAL |
ABSTRACT | CONCRETE | DEFAULT |
REFLECTIVE |
HISTORY | RETAINED | SHALLOW |
CONCURRENT |
NORMALSupporting classes
These are keyed here as a placeholder, and will be valued with a forward import at the end of this class definition.
Metaobject: null
Expression: nullConstructor
The constructor is limited to setting the references that define this
State’s relations, and then computing its attributes bit mask based on
those relations and the intrinsic heritability of each attribute.
Only afterward is the State’s Metaobject, as defined in a provided
expression, then initialized into the instance, if necessary.
constructor: ( base, @name, expression ) ->Relation definitions
The base argument can specify either a superstate from which to inherit,
or an owner for which to act as a new root state.
if base instanceof State
then superstate = base; root = superstate.root; owner = root.owner
else superstate = null; root = this; owner = base
@owner = owner
@root = root
@superstate = superstate
@protostate = protostate = @getProtostate() or nullThe linearized resolution order for this will be set later, either when the
instance is realized, or on demand if a lookup is performed and the order
is not set.
@order = nullAttribute inheritance masking
Explicitly defined literal attributes for this state are encoded as a bit
field integer within expression, and then superimposed atop the inherited
attribute values acquired from this state’s superstate and protostate.
attributes = expression?.attributes or NORMALThe mutable and finite attributes can be inherited from the superstate
straight away.
if superstate?
superAttr = superstate.attributes
attributes |= superAttr & MUTABLE_OR_FINITEA subset of the attributes may be inherited from protostates.
if protostate?
protoAttr = protostate.attributes & PROTO_HERITABLE_ATTRIBUTESLiteral concrete forcibly contradicts literal abstract; if a bad production
includes both attributes, negate abstract.
attributes &= ~ABSTRACT if attributes & CONCRETELiteral abstract may override inherited concrete, and vice versa, so filter
those attributes out of the protostate before inheriting.
if attributes & ABSTRACT_OR_CONCRETE
protoAttr &= ~ABSTRACT_OR_CONCRETE
attributes |= protoAttrIf at this point the state is not abstract, then it is definitely concrete.
attributes |= CONCRETE if ~attributes & ABSTRACTLiteral or inherited immutable contradicts mutable absolutely, and implies
finite.
attributes |= ( superAttr | protoAttr ) & IMMUTABLE
if attributes & IMMUTABLE
attributes = attributes & ~MUTABLE | FINITE
@attributes = attributesOffloaded initialization
@initialize expression unless attributes & VIRTUALDebug properties
For easy viewing in the inspector.
if env.debug
@[' <path>'] = @path()
@['<attributes>'] = StateExpression.decodeAttributes attributesPrivate entities
createDispatcher
For each method defined in any of the owner’s states, a dispatcher must be
created and assigned on the owner itself at the methodName key. Calls to
owner.methodName are then delegated by the dispatcher to the owner’s current
state, from which the appropriate implementation for the method will be located
and applied, and its result returned to the call site.
createDispatcher = do ->
toString = -> "[dispatcher]"
( accessorName, methodName, original ) ->
dispatcher = -> @[ accessorName ]().apply methodName, arguments
dispatcher.isDispatcher = yes
dispatcher.toString = toString if env.debug
dispatcher.original = original if original
dispatcher initialize: ( expression ) ->
{ attributes } = this
return if attributes & VIRTUAL
@attributes |= INCIPIENT
@realize expression
@attributes &= ~INCIPIENT
@emit 'construct', expression, VIA_PROTO
thisrealize
Transforms an incipient or virtual State into a real state.
Initialization of a State’s contents is offloaded from the
constructor via
initialize to here.
realize: ( expression ) ->
{ attributes, name } = this
return this unless attributes & INCIPIENT_OR_VIRTUALBegin realization higher in the superstate chain if necessary, adding each
newly realized state to the substates collection of its real superstate.
if attributes & VIRTUAL
if ss = @superstate
do ss.realize if ss.attributes & VIRTUAL
substates = ss._.substates ?= {}
do substates[ name ].destroy if substates[ name ]?
substates[ name ] = this
@attributes &= ~VIRTUALA State’s Metaobject is stored in an underscore _ property; the
existence of _ in this implies that this must be a realized state.
@_ ?= new @MetaobjectNormalize any declarations of parastates. These will be used later in the
outer realize function to recursively compute a linearization for this
state and for its descendant substates.
if parastates = expression?.parastates
if isArray parastates then parastates = parastates.join ','
throw TypeError unless typeof parastates is 'string'
parastates = parastates.split /\s*,\s*/
if parastates.length
@_.parastates = parastates
@attributes |= PARASTATICPopulate the rest of the empty metaobject by “mutating” against expression.
@mutate expression if expression?Realizing a root state requires that, for each of the owner’s own methods, if there exists at least one stateful implementation of that method located higher in the owner’s prototype chain, then the owner’s implementation of that method must be copied into the root, where it defines the owner’s default behavior.
if this is @root then for own key, method of @owner
@addMethod key, method if key isnt 'constructor' and
typeof method is 'function' and not method.isDispatcher and
@method key, VIA_PROTOThe superstate and parastates of this State are linearized into a defined
order. Because the States of a state tree are constructed bottom-up, in the
case of an incipient state, the linearization step must be deferred to the
root, which will then recursively linearize its descendants from the top
down, after each one has been instantiated and situated in the tree.
@linearize VIA_SUB if this is @root or ~attributes & INCIPIENT
thisvirtualize
Creates, if necessary, a virtualized epistate of this protostate within
the state tree to which inheritor belongs, and also creates as many virtual
ancestor superstates as necessary to reach a real State within that tree.
Returns the state on inheritor’s state tree for which this is a protostate.
This will be the newly created virtual state, unless virtualization was
unnecessary, in which case it will be the extant real epistate of this.
virtualize: ( inheritor ) ->Verify relation between respective owners of inheritor and this.
return null unless inheritor instanceof State and
@owner.isPrototypeOf inheritor.ownerGet the derivation list for this.
return null unless ( derivation = @derivation yes ).lengthTraverse the real states of the inheriting state tree to their furthest depth.
i = 0; s = inheritor.root
while name = derivation[ i++ ]
break unless real = s.substate name, VIA_NONE
s = realIf derivation extends beyond the inheriting state tree’s real states, then
add virtual states to it until the whole superstate chain is represented.
expr = attributes: VIRTUAL
while name
s = new State s, name, expr
name = derivation[ i++ ]
slinearize
Computes, records, and returns the array of States that define the order of
resolution, or linearization, for inheritance of this amongst itself and
ancestors that share a common owner (and thus inhabit a common state tree).
This method adapts the C3 linearization algorithm to the State model,
where the returned list consists of this followed by a monotonic ordering of
its parastate and superstate ancestors.
By definition a State’s protostates are excluded from order. Traversal
VIA_ALL proceeds over the concatenation of the protostate chains for each
State in order; this set inherently preserves the monotonicity of order.
Parastates are specified by the selector paths contained in the parastates
array of the metaobject this._. Epistates inherit any parastate paths
defined by their protostates, and may override their ordering.
linearize: do ->getParastateDeclarations
Returns the concatenation of this State’s own and protostate-derived
declarations of parastates.
getParastateDeclarations = ->
head = @_?.parastates
tail = getParastateDeclarations.call ps if ps = @protostate
if tail? then ( head?.concat tail ) ? tail else headmerge
Creates and returns a monotonic ordered set of States from the provided
lists, where lists is an array of arrays, consisting of an array of
“parent” states, preceded by arrays that represent, in order, the linearization
of each “parent” state.
merge = ( out, lists ) ->
return out unless lists.length
for headList, index in lists when headList?
head = headList[0]; bad = no
for otherList in lists when otherList isnt headList
i = 1; while item = otherList[ i++ ]
if item is head then bad = yes; break
break if bad
continue if bad
out.push head
remainingLists = []; for list in lists
do list.shift if list[0] is head
remainingLists.push list if list.length
return merge out, remainingLists
throw new TypeError "Ambiguous resolution order for '#{ out.pop() }'"linearize
Computes the linearization of this from its named parastates and referenced
superstate, and saves this array as this.order.
linearize = ( via = VIA_NONE ) ->
if this is @root then order = [this] elseDetermine the ordered set of States from which this inherits. By rule any
parastates precede the superstate. Declarations of parastates include those
inherited from protostates; all parastates are resolved to unique own
States of the prevailing owner’s state tree.
parents = []
if paths = getParastateDeclarations.call this
{ owner } = this
for path in paths
unless parastate = state.own owner, path
throw new ReferenceError "Unresolvable parastate '#{ path }'"
parents.push parastate unless parastate in parents
parents.push @superstateCreate an array of the linearizations for each parent, followed by a list of
the parents themselves, then merge each of these lists to create a single
ordered set that defines the monotonic linearization of this.
lists = []
for parent in parents
lists.push ( parent.order ? parent.linearize() )[..]
lists.push parents
order = merge [this], listsSet the determined order, then recurse downward if so directed (e.g., this is
instigated from the root state as the last step of realization, after all of
its substates and descendants are in place).
@order = order
s.linearize via for own name, s of @_.substates if via & VIA_SUB
orderexpress
Returns an expression of this state — a data structure that contains an
exported snapshot of the state’s own contents.
By default the returned expression is returned as a plain object; if typed
is truthy, the expression is a formally typed StateExpression.
express: do ->
cloneCategory = ( object ) ->
return unless object?
( out = {}; break ) for key of object
if out then for key, value of object
out[ key ] = if value and typeof value is 'object'
then clone value
else value
out
cloneEvents = ( events ) ->
return unless events?
( out = {}; break ) for type, emitter of events when emitter
for type, emitter of events when emitter
out[ type ] = clone emitter.items
out
cloneSubstates = ( substates, typed ) ->
return unless substates?
( out = {}; break ) for name of substates
for name, substate of substates
out[ name ] = substate.express typed
out
return express = ( typed ) ->
if _ = @_ then expression = edit {}, { # Why `edit`???
@attributes
data : cloneCategory _.data
methods : cloneCategory _.methods
events : cloneEvents _.events
guards : cloneCategory _.guards
states : cloneSubstates _.substates, typed
transitions : cloneCategory _.transitions
}
if typed then new @Expression expression else expressionmutate
Transactionally mutates this state by adding, updating, or removing items as
specified by the expression provided in expr.
mutate: do ->
{ NIL, isArray, isEmpty, isPlainObject, edit, diff } = O
editEvent = ( object, emitter ) ->
{ items } = emitter
for own key, value of object
if value is NIL then emitter.remove key
else if value and value isnt items[ key ]
emitter.set key, value
return mutate = ( expr ) ->
{ attributes, Expression } = thisPreparation steps
Booleans to determine whether mutation of particular categories is permissible; all content is mutable for the special case of a state being initialized.
incipient = attributes & INCIPIENT
return if not incipient and attributes & IMMUTABLE
mutable = incipient or attributes & MUTABLERealize this state if necessary, then load the category collections.
do @realize if attributes & VIRTUAL
{ data, methods, events, guards, substates, transitions } = @_Validate the provided state expression.
expr = new Expression expr unless expr instanceof ExpressionHold onto an expression of a state’s contents before the mutation, to be used
for comparison later on the mutate event. This step does not pertain to an
incipient state, which will be “mutating” against nothing, and so will
suppress the emission of a mutate event.
before = @express() unless incipientSince mutate is transactional, the ATOMIC flag must be used to signal the
methods utilized here that add or remove content to temporarily suppress their
usual emission of a mutate event.
@attributes |= ATOMICData mutations
Data is already set up to handle differentials that contain NIL values.
@data expr.data if expr.dataMethod mutations
Methods are stored as a simple key mapping, and addMethod can be used both to
create an entry and to update an existing entry, without any additional
side-effects, so method expressions can simply be compared against the NIL
value.
if mutable then for own name, method of expr.methods
if method isnt NIL
then @addMethod name, method
else @removeMethod nameEvent mutations
Event listeners for a given event type might be expressed as either:
- a simple
Arrayof items to be added; - a plain
Objectthat maps items to specific eventids in the internal emitter that should be added, updated, or deleted; or - an
Arraythat also includes one or more suchObjects.
if mutable then for own type, event of expr.events
events or = @_.events or = {}
emitter = events[ type ]
if event is NIL then do emitter?.empty; continueIf event contains items to be added, and an emitter does not already exist
for this event type, then one must be created.
if not emitter and event and not isEmpty event
emitter = events[ type ] = new StateEventEmitter this, type
if isArray event
then for element in event when element? and element isnt NIL
if isPlainObject element
then editEvent element, emitter
else @addEvent type, element
else editEvent event, emitter if isPlainObject event
unless emitter.length
do emitter.destroy
delete events[ type ]Guard mutations
Guards are stored as simple objects, and altering them causes no side-effects,
so a deep edit is sufficient.
if mutable and expr.guards
guards or = @_.guards or = {}
edit 'deep', guards, expr.guardsSubstate mutations
Substates are instances of State, which are either created, destroyed, or
recursively updated in place, as specified by expr.states.
By default, a state is weakly immutable, in which case its direct contents cannot be altered, although any of its substates may yet be mutable, so any submutations must therefore still be applied recursively to their corresponding substates.
for own name, stateExpr of expr.substates
if substates and name of substates
if stateExpr is NIL
then @removeSubstate name
else substates[ name ].mutate stateExpr
else @addSubstate name, stateExpr if stateExpr isnt NILTransition mutations
Transitions, as held by a State, are instances of TransitionExpression,
which are either created, deleted, or replaced, as specified by
expr.transitions.
if mutable then for own name, transitionExpr of expr.transitions
if transitions and name of transitions
if transitionExpr is NIL
then delete transitions[ name ]
else transitions[ name ] =
new TransitionExpression transitionExpr
else @addTransition name, transitionExpr if transitionExpr isnt NILCleanup and export
The transaction is complete, so clear ATOMIC to signal the add... methods
to emit individual mutate events as usual.
@attributes &= ~ATOMICFinally the before snapshot is used to acquire the residue of the mutation,
which is emitted as part of a mutate event.
unless incipient
after = @express()
residue = diff before, after
unless isEmpty residue
@emit 'mutate', [ expr, residue, before, after ], VIA_PROTO
thisdestroy
Attempts to cleanly destroy this state and all of its substates. A destroy
event is issued to each state after it has been destroyed.
destroy: ->
{ owner, root, superstate, _ } = this
{ methods, events, substates } = _ if _If a transition is underway that involves any state other than the root, then the state cannot be destroyed.
if transition = root._transition
if this is root then do transition.abort
else return no if ( transition.origin.isIn this ) or
( transition.target.isIn this )Descendant states are destroyed bottom-up.
do substate.destroy for own name, substate of substatesThe final event emitted is destroy.
@emit 'destroy', VIA_PROTO
if events then for key, event of events
do event.destroy
delete events[ key ]When the root state is destroyed, the owner gets back its original methods, and the corresponding dispatcher for each such method is destroyed, along with the owner’s accessor method.
if this is root
for name of methods when dispatcher = owner[ name ]
continue unless dispatcher.isDispatcher
if ownerMethod = dispatcher.original
then owner[ name ] = ownerMethod
else delete owner[ name ]
delete owner[ @accessorName ]The metaobject should be cleared explicitly, as it may contain a cyclical
reference to this at the head of its linearization array. This should be
sufficient to allow garbage-collection since only this should ever hold a
reference to the metaobject.
@_ = nullA flag is set that can be observed later by anything retaining a reference to this state (e.g. a memoization) which would be withholding it from being garbage-collected.
@attributes |= DESTROYEDA non-root state must remove itself from its superstate.
superstate?.removeSubstate @name
yes isVirtual: -> !!( @attributes & VIRTUAL )
isMutable: -> !!( @attributes & MUTABLE )
isFinite: -> !!( @attributes & FINITE )
isStatic: -> !!( @attributes & STATIC )
isImmutable: -> !!( @attributes & IMMUTABLE )
isInitial: -> !!( @attributes & INITIAL )
isConclusive: -> !!( @attributes & CONCLUSIVE )
isFinal: -> !!( @attributes & FINAL )
isAbstract: -> !!( @attributes & ABSTRACT )
isConcrete: -> !!( @attributes & CONCRETE )
isDefault: -> !!( @attributes & DEFAULT )
isReflective: -> !!( @attributes & REFLECTIVE )
hasHistory: -> !!( @attributes & HISTORY )
isRetained: -> !!( @attributes & RETAINED )
isShallow: -> !!( @attributes & SHALLOW )
isConcurrent: -> !!( @attributes & CONCURRENT )Relational methods
substate
Retrieves the named substate of this state. If no such substate exists in the
local state, any identically named substate held on a protostate will be
returned.
substate: ( name, via = VIA_PROTO ) ->First scan for any virtual active substates in the local state tree.
s = @root._current
while s?.attributes & VIRTUAL and ss = s.superstate
return s if ss is this and s.name is name
s = ssOtherwise retrieve a real substate, either locally or from a protostate.
@_?.substates?[ name ] or
via & VIA_PROTO and @protostate?.substate namesubstates
Returns an array of this state’s immediate substates. If the boolean virtual
is true, any active virtual epistates will be included as well.
substates: ( virtual, deep ) ->
result = []Include virtual substates in the returned set, if any are present.
if virtual and ( s = @root._current ) and s.attributes & VIRTUAL and
@isSuperstateOf s
while s and s isnt this and s.attributes & VIRTUAL and
ss = s.superstate
result.unshift s if deep or ss is this
s = ssInclude real substates.
for own name, substate of @_?.substates
result.push substate
result = result.concat substate.substates undefined, yes if deep
resultdescendants
Returns a depth-first flattened array containing all of this state’s descendant substates.
descendants: ( virtual ) -> @substates virtual, yesaddSubstate
Creates a state from the supplied expression and adds it as a substate of
this state. If a substate with the same name already exists, that state is
first destroyed and then displaced.
addSubstate: ( name, expression ) ->
{ attributes } = this
unless attributes & INCIPIENT
return if attributes & FINITE
return unless attributes & MUTABLE
do @realize if attributes & VIRTUAL
substates = @_.substates or = {}
do substate.destroy if substate = substates[ name ]
substate = if expression instanceof State
then expression.realize() if expression.superstate is this
else new State this, name, expression
return null unless substate
substates[ name ] = substate removeSubstate: ( name ) ->
{ attributes } = this
return if attributes & VIRTUAL
substates = @_?.substates
return unless substate = substates?[ name ]
return unless attributes & MUTABLE or substate?.attributes & DESTROYEDIf a transition is underway involving substate, the removal must fail.
return no if ( transition = @root._transition ) and (
substate.isSuperstateOf( transition ) or
substate is transition.origin or substate is transition.target
)Currency must be evacuated before the state can be removed.
@change this, forced: yes if @root._current.isIn substate
delete substates[ name ]
substatederivation
Returns a State array of this state’s superstate chain, starting after the
root state and ending at this. If byName is set to true, a string array
of the states’ names is returned instead.
derivation: ( byName ) ->
results = []; ss = this; while ( s = ss ) and ss = s.superstate
results.push if byName then s.name or '' else s
results.reverse() path: -> @derivation( yes ).join '.'
toString: @::pathdepth
Returns the number of superstates this state has. The root state returns 0,
its immediate substates return 1, etc.
depth: ->
n = 0; s = this
n += 1 while s = s.superstate
ncommon
Returns the least common ancestor of this and other. If this is itself an
ancestor of other, or vice versa, then that ancestor is returned.
common: ( other ) ->
other = @query other unless other instanceof State
if @depth() > other.depth() then s = other; other = this else s = this
while s
return s if s is other or s.isSuperstateOf other
s = s.superstate
nullis
Determines whether this is other.
is: ( other ) ->
other = @query other unless other instanceof State
other is thisisIn
Determines whether this is or is a substate of other.
isIn: ( other ) ->
other = @query other unless other instanceof State
other is this or other.isSuperstateOf thishasSubstate
Determines whether this is or is a superstate of other.
hasSubstate: ( other ) ->
other = @query other unless other instanceof State
other is this or @isSuperstateOf otherisSuperstateOf
Determines whether this is a superstate of other.
isSuperstateOf: ( other ) ->
other = @query other unless other instanceof State
if superstate = other.superstate
this is superstate or @isSuperstateOf superstate
else nodefaultSubstate
Returns the first substate marked default, or simply the first substate.
Recursion continues into the protostate only if no local substates are marked
default.
defaultSubstate: ( via = VIA_PROTO, first ) ->
for s in substates = @substates()
return s if s.attributes & DEFAULT
first or substates.length and first = substates[0]
if via & VIA_PROTO and protostate = @protostate
return protostate.defaultSubstate VIA_PROTO
firstinitialSubstate
Performs a “depth-within-breadth-first” recursive search to locate the most
deeply nested initial state by way of the greatest initial descendant
state. Recursion continues into the protostate only if no local descendant
states are marked initial.
initialSubstate: ( via = VIA_PROTO ) ->
i = 0; queue = [ this ]
while subject = queue[ i++ ]
for s in substates = subject.substates undefined, !!VIA_PROTO
return s.initialSubstate( VIA_NONE ) or s if s.attributes & INITIAL
queue.push s
if via & VIA_PROTO and protostate = @protostate
return protostate.initialSubstate VIA_PROTOgetProtostate
Returns this state’s protostate, the State that both:
- belongs to the nearest possible prototype of
@owner; and - is taxonomically analogous to
this, the inheriting epistate.
If the owner does not share an analogous state tree with its immediate
prototype, or if that prototype’s tree does not contain a State analogous to
this, then the search is iterated up the owner’s prototype chain.
getProtostate: ->
{ getPrototypeOf } = O
{ owner, root } = this
{ accessorName } = root
path = @path()Walk up the prototype chain, and, starting at each prototype’s root state, use
this state’s path to locate the nearest analogous protostate.
prototype = getPrototypeOf owner
while prototype
if protostate = prototype[ accessorName ]? path, VIA_NONE
return protostate
prototype = getPrototypeOf prototype
nullisProtostateOf
Determines whether this is a state analogous to state on any object in the
prototype chain of state’s owner.
isProtostateOf: ( other ) ->
other = @query other unless other instanceof State
if protostate = other.protostate
this is protostate or @isProtostateOf protostate
else noquery
Matches a selector string with the state or states it represents, evaluated
first in the context of this, then its substates, and then its superstates,
until all locations in the state tree have been searched for a match of
selector.
Returns the matched State, or an Array containing the set of matched
states. If a state to be tested against is provided, a Boolean is returned,
indicating whether against is the matched state or is included in the
matching set.
If no matching state is found relative to the context of this state, then the
query is recursed via the substates, superstates, and protostates of this,
unless otherwise directed, such that a uniquely named state can be located by
name alone from anywhere in the state tree.
query: ( selector, against, via, toBeSkipped ) ->
if typeof against is 'number'
toBeSkipped = via; via = against; against = undefined
via = VIA_ALL unless via?A few exceptional cases may be resolved early.
unless selector?
return if against is undefined then null else no
if selector is '.'
return if against is undefined then this else against is this
if selector is ''
return if against is undefined then @root else against is @rootAbsolute wildcard expressions compared against the root state pass immediately.
return yes if against and against is @root and /^\*+$/.test selectorPure ./* expressions should not be recursed.
via &= ~( VIA_SUB | VIA_SUPER ) if /^\.*\**$/.test selectorIf selector is an absolute path, evaluate it from the root state as a
relative path.
if selector.charAt(0) isnt '.'
return @root.query '.' + selector, against, VIA_SUB | VIA_PROTOAn all-. selector must have one . trimmed to parse correctly.
selector = selector.replace /^(\.+)\.$/, '$1'Split selector into tokens, consume the leading empty-string straight away,
then parse the remaining tokens. A cursor reference to a matching State in
the tree is kept, beginning with the context state (this), and updated as
each token is consumed.
parts = selector.split '.'
i = 0; l = parts.length; cursor = this
while cursor
i += 1Upon reaching the end of token stream, return the State currently referenced
by cursor.
return ( if against then against is cursor else cursor ) if i >= lConsume a token.
name = parts[i]Interpret a single wildcard as any immediate substate of the cursor
state parsed thus far.
if name is '*'
return cursor.substates() unless against
return yes if cursor is against.superstate
breakInterpret a double wildcard as any descendant state of the cursor state
parsed thus far.
if name is '**'
return cursor.substates undefined, yes unless against
return yes if cursor.isSuperstateOf against
breakEmpty string, the product of leading/consecutive .s, implies cursor’s
superstate.
if name is '' then cursor = cursor.superstateInterpret any other token as an identifier that names a specific substate of
cursor.
else if next = cursor.substate name then cursor = nextIf no matching substate exists, the query fails for this context.
else breakRecursively descend the tree, breadth-first, and retry the query with a different context.
if via & VIA_SUB
i = 0; queue = [ this ]
while subject = queue[ i++ ]
for substate in subject.substates yes
continue if substate is toBeSkipped
result = substate.query selector, against, VIA_NONE
return result if result
queue.push substateRecursively ascend the tree and retry the query, but skip this subtree during
the subsequent descent, since it’s already been searched.
if via & VIA_SUPER
return result if result = @superstate?.query( selector, against,
via & VIA_SUB | VIA_SUPER, this if via & VIA_SUB )Retry the query on the protostate.
if via & VIA_PROTO
return result if result = @protostate?.query selector, against, viaAll possibilities exhausted; no matches exist.
return if against then no else null$
Convenience method that either aliases to change if passed a function for the
first argument, or aliases to query if passed a string — thereby mimicking
the behavior of the object’s accessor method.
$: ( expr, args... ) ->
if typeof expr is 'function'
return @change.apply this, [ expr ].concat args if expr = expr()
else if typeof expr is 'string' and
( match = expr.match rxTransitionArrow ) and
method = transitionArrowMethods[ match[1] ]
return if args.length
then @[ method ].apply this, [ match[2] ].concat args
else @[ method ] match[2]Currency methods
Methods that inspect or affect the owner’s current state.
current
Gets the local state tree’s current state, which is authoritatively determined by the root state.
current: -> @root._current isCurrent: -> this is @current()isActive
Returns a Boolean indicating whether this or one of its substates is the
owner’s current state.
isActive: -> this is ( current = @current() ) or @isSuperstateOf currentchange
Forwards a change command to root and returns its result. Calling with no
arguments implicitly directs the root to change to this state.
Aliases: go, be
change: ( target, options ) ->
( root = @root ).change.apply root, arguments
go: @::change
be: @::changeData methods
data
Either retrieves or edits a block of data associated with this state.
If provided no argument, or an integer bit field, then data returns a copy
of the data attached to this state, including all data from inherited states,
unless specified otherwise by the via query flags.
If called with an object-typed argument, data edits the data held on this
state. For keys in mutation whose values are set to the NIL directive, the
matching keys in the state’s data are deleted. If the operation results in a
change to the state’s data, a mutate event is emitted for this state.
data: ( via = VIA_ALL, out ) ->If the provided via argument is not a flags integer mask, and presumably an
object instead, then interpret this call as a write operation, and refer to
the parameter as mutation.
mutation = via if via isnt via << 0
if mutation
{ attributes } = this
if attributes & INCIPIENT_OR_MUTABLE and not isEmpty mutation
return @realize().data mutation if attributes & VIRTUAL
residue = delta @_.data ?= {}, mutation
if ~attributes & ATOMIC and residue and not isEmpty residue
@emit 'mutate', [ mutation, residue ], VIA_PROTO
thisOtherwise read and return a copy of this state’s data, including data
inherited via superstates and protostates, unless directed otherwise.
else
out ?= {}
for relative in @order ? @linearize() by -1
continue unless via & VIA_SUPER or relative is this
edit 'deep all', out,
( relative.protostate?.data VIA_PROTO, out if via & VIA_PROTO ),
relative._?.data
outhas
Determines whether a property with the given key is contained within this
state’s data storage, or that of an ancestral protostate, parastate, or
superstate, unless directed otherwise by via.
has: ( key, via = VIA_ALL ) ->
viaSuper = via & VIA_SUPER
viaProto = via & VIA_PROTO
for relative in @order ? @linearize()
s = relative; while s?
return yes if ( data = s._?.data )? and hasOwn.call data, key
if viaProto then s = s.protostate else break
continue if viaSuper
noget
Retrieves the value of a key within this state’s data storage, or from
that of an ancestral protostate, parastate, or superstate, unless directed
otherwise by via.
get: ( key, via = VIA_ALL ) ->
viaSuper = via & VIA_SUPER
viaProto = via & VIA_PROTO
for relative in @order ? @linearize()
s = relative; while s?
if ( data = s._?.data )? and hasOwn.call data, key
return data[ key ]
if viaProto then s = s.protostate else break
continue if viaSuperlet
Assigns a value to a key within this state’s data storage. If no such
key already exists, it is added.
let: ( key, value ) ->
{ attributes } = this
return unless attributes & INCIPIENT_OR_MUTABLE # should warn
do @realize if attributes & VIRTUALAssignment proceeds only if the value being written is not the same as the
displaced data that is being overwritten.
data = @_.data or = {}
if value isnt displaced = lookup data, key
assign data, key, value
assign ( mutation = {} ).data = {}, key, value
assign ( residue = {} ).data = {}, key, displaced
@emit 'mutate', [ mutation, residue ], VIA_PROTO
valueset
Assigns a value to an existing key within the data storage of the nearest
mutable ancestral parastate or superstate of this. Returns the equivalent of
a let operation on this if no such ancestor can be affected.
Only States that belong to this.owner can be affected by set; their
protostates cannot.
set: ( key, value ) ->
{ attributes } = this
return unless attributes & INCIPIENT_OR_MUTABLE
do @realize if attributes & VIRTUALFind the nearest State along the linearization path of this whose data
storage contains a property with the given key, and mutate that property with
the given value if allowed to do so. If these steps do not complete, delegate
to let.
for relative in @order ? @linearize()
if ( data = relative._?.data )? and hasOwn.call data, key
return relative.let key, value if relative.attributes & MUTABLE
break
@let key, valuedelete
Removes a property from this state’s data storage, provided that this is
mutable.
delete: ( key ) ->
return unless @attributes & MUTABLE
NIL is @let key, NILMethod methods
method
Retrieves the named method for this state. Providing an optional out object
allows the appropriate context for a state-bound method to be delivered as a
property of out; if included, context is confined to the local state tree.
method: ( methodName, via = VIA_ALL, out, returnBoxed ) ->
{ attributes } = this
realized = ~attributes & VIRTUALDuring the pseudo-loop block, the
contextreference should be considered provisional, because its potential value is in part a product of the manner in which its accompanyingmethodwas retrieved. Afterbreaking out of the block, ifmethodis a state-bound function, thencontextwill be either kept and exported viaout, or discarded otherwise.
loop # onceFirst seek a local or memoized inherited implementation of the named method.
if realized
if method = @_?.methods?[ methodName ]
context = this
break
if record = @_?.__dispatch_table__?[ methodName ]
[ method, context ] = record
break if method?If an implementation is not available locally, seek an inherited method along
the protostate chain. If this succeeds, the provisional context will be the
epistate that inherits the method.
if viaProto = via & VIA_PROTO
if method = @protostate?.method methodName, VIA_PROTO, out, yes
context = this
inherited = yes
breakIf no local or protostatic method exists, seek an inherited method along the
linearization path. If this succeeds, the provisional context will be the
superstate or parastate from which the method is inherited.
if via & VIA_SUPER
for relative in @order ? @linearize() when relative isnt this
if method = relative.method methodName, viaProto, out, yes
context = out?.context ? null
inherited = yes
break
break if method?The method does not exist.
context = null
break # always
if method?If method is a function, it is not state-bound, so context is unnecessary.
if typeof method is 'function'
context = nullIff this is a realized State, inherited lookup results can be memoized in
the local dispatch table.
if realized and inherited and useDispatchTables
table = @_.__dispatch_table__ ?= {}
table[ methodName ] = [ method, context ]Unbox a state-bound function unless directed otherwise.
unless returnBoxed
method = method.fn if method.type is 'state-bound-function'Export method and context together if the out reference was provided.
Callers who know that
methodwill be unbound need not provide anout.
if out?
out.method = method
out.context = context
method methodNames: ->
keys methods if methods = @_?.methodsaddMethod
Adds a method to this state. The provided fn may be any of:
- A proper function, which will be called in the context of
@owner; - A boxing of a bound function as prepared by
state.bind, which will cause the boxed function to be called in the context of eitherthisStateor the epistate inheriting the method; - A boxing of a fixed function as prepared by
state.fix, whose boxed function will be closed over hard references tothisasautostate, and to the protostate ofthis, asprotostate.
If a method called methodName does not already exist in the state tree, then
the owner is provided a dispatcher to accommodate calls to the appropriate
state’s implementation of this method.
addMethod: ( methodName, fn ) ->
return unless @attributes & INCIPIENT_OR_MUTABLEIf fn boxes a state-fixed function, then partially apply that function to
extract the actual method, closed over references to the locality of this.
if typeof fn is 'object' and fn.type is 'state-fixed-function'
fn = fn.fn this, @protostate
unless typeof fn is 'function' or fn?.type is 'state-bound-function'
throw new TypeError "Must supply a plain, bound, or fixed function"
{ owner } = thisSkip ahead if the owner is already set up with a dispatcher for this method.
unless ( ownerMethod = owner[ methodName ] )?.isDispatcher
{ root } = thisCreate a new dispatcher method for owner. Its original method, if it has one,
will be retained by the dispatcher, so that if the state tree is destroyed
later, the method can be reinstated on owner.
owner[ methodName ] =
createDispatcher root.accessorName, methodName, ownerMethodUnless we’re adding directly to root, copy ownerMethod to root — which
will not have an implementation for this method. (If it did, then ownerMethod
would already be a dispatcher.) From the root state it can still serve as
owner’s default implementation, available to any of its States that do not
override that method.
if ownerMethod? and this isnt root
methods = root._?.methods or = {}
methods[ methodName ] = ownerMethod
methods = @_?.methods or = {}
methods[ methodName ] = fn removeMethod: ( methodName ) ->
return unless @attributes & MUTABLE and ( methods = @_?.methods ) and
fn = methods[ methodName ]
delete methods[ methodName ]
fn hasMethod: ( methodName ) ->
method = @method methodName hasOwnMethod: ( methodName ) ->
!! @method methodName, VIA_NONEapply
Finds a state method and applies it in the appropriate context.
If the named method does not exist locally and cannot be inherited, then
noSuchMethod events are emitted, and the call returns undefined.
apply: ( methodName, args ) ->First try to resolve the method quickly from the local dispatch table.
if record = @_?.__dispatch_table__?[ methodName ]
[ method, context ] = record
method = method.fn if method?.type is 'state-bound-function'Resort to a proper lookup if the fast way turns up nothing.
unless method?
if method = @method methodName, VIA_ALL, out = {}
{ context } = outBail out gracefully if the method definitively cannot be resolved.
else
@emit 'noSuchMethod', [ methodName, args ]
@emit 'noSuchMethod:' + methodName, args
returnIf at this point a context is provided, this means that method is a
state-bound function, and context will be the appropriately bound State. If
no context exists, this means that method is a typical non-state-bound
function, which is meant to be invoked in the usual fashion, just as if it were
called directly as a method of this state’s owner.
method.apply context or @owner, args call: ( methodName, args... ) ->
@apply methodName, argsEvent methods
event
Returns a registered event listener, or the number of listeners registered, for
a given event type.
If an id as returned by addEvent is provided, the event listener associated
with that id is returned. If no id is provided, the number of event
listeners registered to type is returned.
event: ( eventType, id ) ->
return unless emitter = @_?.events?[ eventType ]
return emitter.length if id is undefined
id = emitter.key id if typeof id is 'function'
emitter.get idaddEvent
Binds an event listener to the specified eventType and returns a unique
identifier for the listener.
Alias: on
addEvent: ( eventType, fn, context ) ->
do @realize if @attributes & VIRTUAL
events = @_.events or = {}
unless hasOwn.call events, eventType
events[ eventType ] = new StateEventEmitter this
if fn.type is 'state-fixed-function'
fn = fn.fn this, @protostate
events[ eventType ].add fn, context
on: @::addEventremoveEvent
Unbinds the event listener with the specified id that was supplied by
addEvent.
Alias: off
removeEvent: ( eventType, id ) ->
@_?.events?[ eventType ].remove id
off: @::removeEventemit
Invokes all listeners bound to the given event type. Callbacks inherited from
protostates, parastates, and superstates are also invoked, unless otherwise
directed by the traversal flags of via.
Arguments to be supplied to callbacks can be passed as an array to args.
Unless context is provided explicitly, a provisional State context is
determined for event callbacks that are state-bound functions. This context
will be either this or an epistate that inherits events from this.
Normal, unbound callbacks are invoked in the context of this.owner as usual.
Alias: trigger
emit: ( eventType, args, context, via = VIA_ALL ) ->
return if typeof eventType isnt 'string'
if typeof args is 'number'
via = context; context = args; args = undefined
if typeof context is 'number'
via = context; context = undefined
args = [args] if args? and not isArray argsProvisional context is flattened onto this.owner’s state tree.
@_?.events?[ eventType ]?.emit args, context ? this
if via & VIA_PROTO
@protostate?.emit eventType, args, context ? this, VIA_PROTO
if via & VIA_SUPER
for relative in @order ? @linearize() when relative isnt this
relative.emit eventType, args, context ? relative
return
trigger: @::emitGuard methods
guard
Gets a guard entity for this state. A guard is a value or function that
will be evaluated during a transition to determine whether an owner’s currency
will be admitted into or released from the State to which the guard is
applied.
Guards are inherited from protostates, but not from parastates or superstates.
guard: ( guardType ) ->
if guard = @_?.guards?[ guardType ] then clone guard
else @protostate?.guard( guardType ) or undefined addGuard: ( guardType, guard ) ->
{ attributes } = this
return unless attributes & INCIPIENT_OR_MUTABLE
do @realize if attributes & VIRTUAL
guards = @_.guards or = {}
edit guards[ guardType ] or = {}, guard removeGuard: ( guardType, args... ) ->
{ attributes } = this
return if attributes & VIRTUAL
return unless attributes & MUTABLE and guards = @_.guards
return null unless guard = guards[ guardType ]
return ( guard if delete guards[ guardType ] ) unless args.length
for key in flatten args when typeof key is 'string'
entry = guard[ key ]
return entry if delete guard[ key ]Transition methods
A State may hold transition expressions that describe a Transition
involving itself or any descendant State.
transition
Returns the named transition expression held on this state.
transition: ( name ) -> @_?.transitions?[ name ] transitions: -> clone @_?.transitions addTransition: ( name, expression ) ->
{ attributes } = this
return unless attributes & INCIPIENT_OR_MUTABLE
do @realize if attributes & VIRTUAL
unless expression instanceof TransitionExpression
expression = new TransitionExpression expression
transitions = @_.transitions or = {}
transitions[ name ] = expression removeTransition: ( name ) ->
{ attributes } = this
return if attributes & VIRTUAL
return unless attributes & MUTABLE and transitions = @_.transitions
transition = transitions[ name ]
delete transitions[ name ] if transition
transitionState::Metaobject = require './state-metaobject'
State::Expression =
StateExpression = require './state-expression'
StateEventEmitter = require './state-event-emitter'
TransitionExpression = require './transition-expression'