Installation
state.js 0.2.0 (104kB)16.5kB min/gz
In Node.js
State can be installed as a Node.js module via npm:
In the browser
State can be included using your favorite package manager, or directly:
which will expose the module at window.state
. You may wish to avoid the global reference and instead hold State within a closure by calling noConflict
:
Getting started
Step 0 — The state
function
The State module is exported as a function named state
, which can be used for either of two purposes:
Implementing state into an object
Given two object-typed arguments owner
and expression
, calling state
will augment owner
with its own working state implementation based on the contents of expression
(and any keywords included in the optional attributes
string). The newly stateful owner
’s initial state is returned.
Expressing a state’s content
Given a single expression
object (and optional attributes
), calling state
will create and return a state expression that describes the intended content of a state. This usage of state
is most often employed within the expression
argument of an outer state
call, to define constituent substates.
Step 1 — Building a state expression
The state
function’s expression
argument, usually an object literal, describes the constituent states, methods, and other features that will form the state implementation of its owner:
Here, person
is the owner, greet
is its method, and Formal
and Casual
are states, inside each of which is a stateful method that will override person.greet
.
Step 2 — Accessing an object’s state
After calling state
to implement state into person
, the new state implementation will be exposed through a special accessor method at person.state
.
Calling the accessor method with no arguments queries the object for its current state:
In this case the current state of person
is its top-level root state, whose name is always the empty string ''
. While person
is in this state it will exhibit its default behavior:
Step 3 — Transitioning between states
The object’s current state may be reassigned to a different state by calling its change
method and providing it the name of a state to be targeted. Transitioning between states allows an object to exhibit different behaviors:
A sugary alternative to change()
is to prepend a transition arrow to the targeted state’s name, and pass this string into the accessor method:
- A naked transition arrow implies a transition that targets the root state (
''
).
Overview
The points listed here summarize the discussions that follow in the Concepts section.
-
States — A state acts on behalf of an owner object to describe the owner’s behavior at a given moment. The owner is able to express and alter its various behaviors by occupying and transitioning between its states.
-
Object model —
State
s may inherit and be composed from otherState
s. The hierarchical superstate–substate relation defines a state tree rooted from the owner’s unique root state. The compositional parastate relation further provides linearized multiple inheritance over the owner’s state tree.State
s also observe indirect prototypal inheritance via the protostate–epistate relation implied by State implementations on any prototype of the owner. -
Expressions — A state expression describes the contents of a state. States may be expressed concisely with an object literal, which, along with an optional set of attribute keywords, can be passed into the
state()
function. There the provided input is interpreted into a formalStateExpression
, which can then be used to createState
instances. -
Selectors — An
owner
’s accessor methodowner.state()
can be called without arguments to retrieve the object’s current state, or, if provided a selector string, toquery
for a specificState
or a specific set ofState
s. -
Attributes — A state expression may include a set of attribute keywords (e.g.:
mutable
,initial
,conclusive
,abstract
, etc.), which will enable features or impose constraints for theState
that the expression is to represent. -
Data — Arbitrary data can be attached to each
State
, and will be inherited accordingly through protostates, parastates, and superstates. -
Methods — Behavior is modeled by defining state methods that override the owner’s methods. Method calls on the owner are dispatched automatically to the proper implementation, given the owner’s current state. Consumers of the owner can therefore call its methods as usual, agnostic to what its current state is, or even to the existence of any formal concept of “state”.
-
Transitions — When an object is directed to change from one state to another, it does so by temporarily entering into a transition state. A state expression may include transition expressions that describe, given a specific pairing of origin and target states, a synchronous or asynchronous action to be performed over the duration of the transition.
-
Events — A
State
accepts listeners for specific event types, which will be called as theState
is affected by a progressing transition, as theState
itself experiences changes to its content, or upon theState
’s construction or destruction. State also allows for the definition of custom typed events, which can be emitted from a particular state and propagated to listeners bound to the state itself as well as its protostates and superstates. -
Guards may be applied to a state to govern its viability as a transition target, dependent on the outgoing state and any other conditions that may be defined. Likewise guards may also be included in transition expressions, where they are used to select a particular transition to execute. Guards are evaluated as predicates if supplied as functions, or as static boolean values otherwise.
Concepts
States
Instances of State
encapsulate the condition and behavior of an owner object at a given moment. A State
is comprised of collections of methods, arbitrary data, events, guards, substates, and transition expressions.
An owner usually bears multiple State
s, and occupies one of these as its current state, during which the owner’s methods will exhibit any behaviors described by that state. Differential behavior is then expressed by instigating a transition, which moves the owner’s currency from the previously current state to the transition’s target state.
Object model
State
objects are modeled by a set of relational references that define three distinct dimensions or “axes”.
Fundamentally the State
model is hierarchical, describing a rooted tree of State
s belonging to a unique owner object. Starting with the owner’s unique root state, each State
may serve as a superstate from which any number of substates inherit.
A State
may also inherit from zero or more parastates, providing a means to define compositional relations, implemented via C3 linearized multiple inheritance.
An owner’s state tree is further heritable by any prototypal inheritors of that owner, which view their prototype’s State
s as protostates from which their own epistates inherit.
Resolving inherited content for a given State
S follows the fundamental relation precedence:
- the protostate chain of S
- the parastates of S, in declared order
- the superstate of S
where the full depth of all parastate and superstate ancestors of S is linearized into a resolution order, or “parastate–superstate chain”, and the protostate chain of each State
in this list is traversed in turn, thereby covering the entire ancestry of S, in all dimensions, and in regular, monotonic order.
The root state
All State–affected owner objects bear a single root state. The root state’s name
is uniquely identified as the empty string ''
.
By default the owner’s initial current state will be set to the root state — unless the root state is marked with the abstract
attribute, or another State
in the tree is marked with the initial
attribute.
The root state also serves as a store for methods of the owner which are overridden by any of the owner’s State
s. Such methods are swapped into the root state, and replaced on the owner with dispatchers, which delegate calls received by the owner to the owner’s current state. From this it follows that the owner will exhibit its default behavior whenever its root state is current.
SEE ALSO
Superstates and substates
An owner object’s expressed behavior is specified by substates, and conversely generalized by superstates.
The superstate axis — A stateful owner bears a rooted state tree. Each
State
in the tree may bear zero or more substates, and accordingly trace a superstate chain up to the uniqueRootState
. Most of aState
’s content, including methods, data, events, etc. may be inherited from superstates and extended or overridden by substates.
A notable distinction of State from other hierarchical state implementations is that an owner’s currency is not necessarily confined to “leaf” states. The owner is free both to exhibit specific behavior by transitioning to a state nested deeper in the tree, and also to exhibit more generic behavior by transitioning to a concrete interior superstate.
SEE ALSO
Parastates and composition
Alongside conventional single inheritance via superstates, the State model also provides multiple inheritance with the parastate relation. State
s inherit directly from zero or more parastates, followed by exactly one superstate.
The lone exception to this is a
RootState
, which bears neither relation.
Parastates are declared with the state.extend
function, which takes a string of one or more paths to parastates
, along with optional parameters attributes
and expression
, and returns a StateExpression
that can be used to produce a State
with the named parastates.
State
AA
inherits conventionally from superstateA
, but not before inheriting compositionally from parastatesX
andY
.
Linearization, inheriting precedence, and monotonicity
The resolution order by which a State
inherits from its lineage of parastate and superstate ancestors is guaranteed to be unambiguous. It is also guaranteed to be monotonic relative to the resolution order for any descendants of the State
:
-
Given
State
S with ancestorState
s A and B (either parastates or superstates), where A precedes B in the resolution order of S; -
A
State
T, to which S is related as either a parastate or superstate of T, is guaranteed to encounter A before B in its own resolution order.
These assertions are enforced by an implementation of the C3 linearization algorithm (Dylan, Perl, Python) — with the State–specific stipulations that:
-
A
State
’s “parents” are defined and ordered by its immediate parastates, in declared order, followed by its immediate superstate. -
A
RootState
by rule cannot inherit from parastates, and by definition does not inherit from a superstate.
Parastate–superstate graph and linearization — from the example code above, parastates
X
andY
ofState
AA
are depicted to the left of its superstateA
, indicating the superstate’s intrinsic position as the “final parent” of aState
. The linearization ofAA
determines the precedence, or resolution order, by which theState
will inherit methods, data, and events defined in its ancestors.
Attempting to implement an expression that produces a State
graph which does not conform to the C3 restrictions will throw a TypeError
.
Prototypal flattening
In the State model, a State
and its parastates must share a common owner
. However, parastate declarations may include paths that resolve to State
s belonging to a prototype of the owner
. In such a case these “proto-parastates” are automatically flattened onto the owner
’s state tree:
-
Given
State
S withowner
O, which has prototype P, where P bears aState
A whose path is'A'
, and given that S declares path'A'
as a parastate relation; -
As O contains no
State
with path'A'
, the protostate A belonging to P will be automaticallyvirtualize
d andrealize
d — effectively flattening it — into the state tree of O as an epistate Aʹ, with path'A'
, such that S may inherit from parastate Aʹ.
Miscellanea
-
The progression of a transition is conceptually orthogonal to the parastate relation, and traversal proceeds only over the state tree defined by the superstate–substate relations.
-
Parastates provide for compositional reuse of only their own or inherited methods, data, and custom events. Built-in events, guards, substates, transitions, and attributes are not heritable via parastate.
SEE ALSO
Protostates and epistates
The relation between an object and its prototype is reflected throughout the State implementation of each. Inheritors of a State–affected prototype view its State
s as their protostates; conversely, matching State
s of an inheritor are epistates of their respective protostates.
This indirect prototypal relation defined by protostates and epistates confers many of the benefits of language-level prototypal reuse patterns to State
s without entangling them in any direct prototypal relationships themselves.
The protostate axis — A particular superstate chain (root–
A
–AA
) is viewed here along the horizontal axis, within the prevailing context of a prototype chain (q
–p
–o
) on the vertical axis. The prototypal relation between these owner objects implicitly defines protostate chains which link analogously-pathedState
s, e.g. (qA
–pA
–oA
) and (pAA
–oAA
), along a parallel vertical axis.
In this diagram the inheriting owner
o
defines no real states of its own, other than the root, however it still views statespA
andpAA
as its protostates, and may inherit these as virtual epistates, indicated by the faded appearance ofoA
andoAA
. In this manner, state content, behavior, etc. defined forp
andq
will also be exhibited byo
, just as if those states had been defined directly ono
itself.
The following example shows an object that, rather than being affected by the state()
function directly, instead inherits from a prototype which already bears a state implementation.
Here
person
, lacking a state implementation of its own, inherits thestate
method from its prototype. Whenperson.state()
is invoked, a new state implementation is automatically created forperson
, which is given its ownstate
method and an emptyRootState
.
Henceforth
person
will automatically inherit all content from its protostates, but will independently maintain its own currency and transitions over the inherited protostates, leaving the currency of the prototype unaffected.
Virtual epistates
When an accessor method (person.state
) is called, it first checks the context object (person
) to ensure that it has its own accessor method. If it does not, and is instead attempting to inherit the accessor (state
) of a prototype, then an empty state implementation is automatically created for the inheritor, which in turn generates a corresponding new accessor method (person.state
), to which the original call is then forwarded. The new state tree of person
will consist only of an empty root state, but this is sufficient to allow the object to inherit from any of its protostates while maintaining its own independent currency.
When an inheritor adopts a protostate as its current state, the currency is borne by a temporary, lightweight virtual epistate that is created in the inheritor’s state tree. Virtual states exist only so long as they are active and necessary; once the object transitions elsewhere, any virtual states consequently rendered inactive are automatically destroyed.
RootState createAccessor
State
constructorState::getProtostate
Expressions
State
instances are defined declaratively using the state expression data structure.
A formal StateExpression
is created by calling the state()
function with no owner
argument, providing it only a plain object map for its expression
argument, optionally preceded by a string of whitespace-delimited attributes
to be encoded into the returned StateExpression
.
Internally, the contents of a state expression are shaped according to a set of objects that represent categories, which include data
, methods
, events
, guards
, substates
, and transitions
. The object map supplied to the state()
call can be structured according to these categories, or it may be pared down to a more convenient shorthand, which, by making certain type inferences, the state()
call will interpret into a properly shaped StateExpression
.
Structured state expressions
Building upon the introductory example, we could write a state expression that consists of explicitly categorized members (substates, methods, events, etc.), looking something like this:
Shorthand
Explicitly categorizing the defined members is unambiguous, but it can be verbose, so state()
also accepts a more concise expression format, which is interpreted into a StateExpression
that is materially identical to the result of the example above:
In this example, the state()
invocation interpreted the input by:
-
recognizing the absence of any items whose keys are category names, and instead inferring that object literals
Formal
andCasual
describe states. -
identifying
enter
as a built-in event type, and thus treating the associated function values as listeners forenter
events that will be emitted by the containing state. -
inferring that functions keyed
greet
, which is not a built-in event type, were to be treated as a method of the containing state.
Explicit definition can also be mixed freely with shorthand in the same expression input, so as to resolve ambiguities in certain edge cases (for example, to create a state named data
, or a method named enter
).
Interpreting expression input
Expression input provided to state()
is interpreted according to the following type inference rules:
-
If an entry’s value is a typed
StateExpression
orTransitionExpression
, interpret it as-is, using the entry’s key as its name, or, if the entry’s value is the exportedstate
function itself, interpret it as an empty state whose name is the entry’s key. -
Otherwise, if an entry’s key is a category name, and its value is either an object or
null
, then it will be interpreted as it would in the long-form structured format. -
Otherwise, if an entry’s key matches a built-in event type or if its value is a string, then interpret the value as either an event listener function, an array of event listeners, or a named transition target to be bound to that event type.
-
Otherwise, if an entry’s key matches a guard action (i.e.,
admit
,release
), interpret the value as a guard condition (or array of guard conditions). -
Otherwise, if an entry’s value is an object, interpret it as a substate whose name is the entry’s key, or if the entry’s value is a function, interpret it as a method whose name is the entry’s key.
Selectors
If called with no arguments, a stateful object
’s accessor method (object.state
) returns the object’s current state. If a selector string argument is provided, the accessor will query the object’s state tree and return a matching State
.
State uses a simple selector format:
-
State names are delimited from their member substates with the dot (
.
) character. -
A selector that begins with
.
is first evaluated relative to the local context state, while a selector that begins with a name will be evaluated as absolute, i.e., relative to the root state. -
A fully-qualified name is not necessary except for disambiguation:
'A.B.C'
and'C'
will both resolve to the deep substate namedC
provided that there is no other state namedC
located higher in the state tree. -
Special cases: empty-string
''
references the root state; single-dot.
references the local context state; double-dot..
references its immediate superstate, etc. -
Querying a selector ending in
*
returns an array of the immediate substates of that level, while**
returns a flattened array of all descendant substates of that level.
Selectors are similarly put to use elsewhere as well: a transition’s origin
and target
properties are evaluated as selectors; and several State
methods, including change
, is
, isIn
, has
, isSuperstateOf
, and isProtostateOf
, accept a selector as their main argument.
Attributes
State expressions may include a space-delimited set of attributes, provided as a single string argument that precedes the object map within a state()
call.
An expression’s attributes modify any State
instance constructed from the expression, so as to enable certain features or impose useful constraints on the state.
Mutability attributes
By default, states are weakly immutable — their data, methods, guards, substates, and transitions cannot be altered once the state has been constructed — a condition that can be affected at construct-time by the mutability attributes mutable
, finite
, and immutable
, listed here in order of increasing precedence.
Declaring a state mutable
allows it and any states that inherit from it to be modified after it is constructed. This can be partially restricted by declaring finite
, which disallows addition or removal of substates. Mutability can be ultimately restricted by declaring a state immutable
, which disallows modification absolutely, for all inheritors.
Each of the mutability attributes is implicitly inherited from any of the state’s ancestors, be they superstates or protostates.
Abstraction attributes
State does not confine currency to “leaf” states. All states, including substate-bearing interior states, are concrete by default, and thus may be targeted by a transition. Nevertheless, sometimes it may still be appropriate to author abstract states whose purpose is limited to serving as a common ancestor from which concrete descendant states will inherit. The abstraction attributes abstract
, concrete
, and default
control these restrictions.
Transitions that target an abstract
state are redirected to its default
substate. If no substate is marked default
, the transition is redirected to the abstract state’s first substate. If the redirection target is itself abstract
, the redirection recurses until a concrete descendant is found.
Each of the abstraction attributes is inherited from protostates, but not from superstates. States may override an abstract
attribute by applying the concrete
attribute.
Destination attributes
An object’s currency must often be initialized or confined to particular states, as directed by the destination attributes initial
, conclusive
, and final
.
The conclusive
attribute traps an object’s currency; once a conclusive state is entered, it cannot be exited, though transitions that take place entirely within the conclusive state may proceed. Similarly, once an object arrives at a final
state, no further transitions are allowed.
Each of the destination attributes are inherited from protostates, but not from superstates.
Data
Arbitrary data can be attached to a state, and inherited accordingly through protostates and superstates. Data properties are declared within a state expression under the data
category. Properties can be read using the get
method. For mutable
states, properties can be added and written to using let
and set
, and removed with delete
. Data can also be manipulated transactionally with the data
method.
Methods
A core feature of State is the ability for an object to exhibit any of multiple well-defined behaviors. This is achieved with state methods, which may override or augment the methods of a State
’s owner anytime that State
is current or active.
Dispatchers
When applied to an owner object by calling state()
, State first identifies any methods already present on the owner for which there exists at least one override somewhere within the provided state expression, and relocates these methods to the new root state. For all state methods, a special dispatcher method is then instated on the owner at the corresponding key.
The dispatcher’s job is to redirect all invocations to the owner’s current state, from which State will then locate and invoke the proper stateful implementation of the method. If no active states contain an implementation for the invoked method, the invocation will be forwarded to the owner’s original implementation of the method, if one exists, or will cause a noSuchMethod
event otherwise.
SEE ALSO
Method context
By default, state methods are invoked just like normal methods, in the context of the receiving owner.
However, certain methods may require awareness of the State
from which it is called, for example, to delegate to a superstate’s implementation of a method. This can be expressed by wrapping a method’s function expression in a call to state.bind
, which causes the method to be invoked in the context of the receiving State
instead of the receiving owner. This exposes reliable references to this.superstate
, this.root
, and any other relative location in the receiving owner’s state tree, along with, importantly, the owner itself via this.owner
.
In this way delegation to a superstate’s method is facilitated by the apply
and call
methods of State
:
Worth noting here is the significant difference distinguishing these apply
and call
methods from their familiar Function.prototype
counterparts: whereas for a function, the first argument accepted by apply
and call
is a context object, for the State::apply
and State::call
methods, the first argument is a string that names a method on that state to be invoked.
SEE ALSO
Lexical bindings
A state method may require awareness of the precise State
in which it is defined, which is necessary for introspection and delegation along the protostate axis.
This can be expressed by enclosing a method’s function expression in a decorator, and passing this to state.fix
— a pattern that lexically binds additional State
context into the method.
Decoration via fix
provides the state-lexical references:
autostate
: the preciseState
in which the method is defined.protostate
: the protostate ofautostate
.
Should a function require insight into both its calling context and its state-lexical environment, this can be composed neatly with both fix
and bind
:
In such a case the distinction and relationship between autostate
and this
is important: if a function is inherited from a protostate, then autostate
will accordingly be a protostate of this
; if the function is not inherited, then autostate
and this
are identical.
SEE ALSO
Handling calls to currently nonexistent methods
In the case of an attempt to call
or apply
a state method that does not exist within that state and cannot be inherited from any protostate or superstate, the invocation will act as a no-op, returning undefined
.
State allows such a contingency to be trapped by emitting a generic noSuchMethod
event. Listeners take as arguments the name of the sought method, followed by an Array
of the arguments provided to the failed invocation.
Also emitted is a specific noSuchMethod:name
event, which includes the name
of the sought method. Listeners of this event take the individual arguments as they were provided to the failed invocation.
SEE ALSO
Examples
These examples demonstrate some of the patterns of state method inheritance. Note the points of interest numbered in the trailing comments and their explanations below:
Document
- A “privileged” method
edit
is defined inside the constructor, closing over a private variabletext
to which it requires access. Later, when state is applied to the object, this method will be moved to the root state, and a dispatcher will be added to the object in its place.
- The
edit
method override defined on theSaved
state is not closed over the constructor’s private variabletext
, but it can still set the value oftext
by usingthis.superstate.apply
to invoke the overridden original implementation.
- The
freeze
method is declared on the abstract root state, callable from statesDirty
andSaved
(but notFrozen
, where it is overridden with a no-op).
- The
save
method, which only appears in theDirty
state, is still callable from other states, as its presence inDirty
causes a no-op version of the method to be automatically added to the root state. This allowsfreeze
to safely callsave
despite the possiblity of being in a state (Saved
) with no such method.
- Changing to
Saved
fromDirty
results in theWriting
transition, whose asynchronousaction
is invoked with the arguments array provided by the call togo
.
Shooter
Transitions
Whenever an object’s current state changes, a transition state is created, which temporarily assumes the role of the current state while the object is travelling from its origin or source state to its target state.
Transition expressions
A state expression may include any number of transition expressions, which define some action to be performed, either synchronously or asynchronously, along with selectors for the origin
/source
and target
states to which the transition should apply, and guards to determine the appropriate transition to employ.
Before an object undergoes a state change, it examines the transition expressions available for the given origin and target, and selects one to be enacted. To test each expression, its origin
state is validated against its admit
transition guards, and its target
state is validated against its release
transition guards. The object then instantiates a Transition
based on the first valid transition expression it encounters, or, if no transition expression is available, a generic actionless Transition
.
Where transition expressions should be situated in the state hierarchy is largely a matter of discretion. In determining the appropriate transition expression for a given origin–target pairing, the search proceeds, in order:
- at the expression’s
target
state (compare to the manner in which CSS3 transitions are declared with respect to classes) - at the expression’s
origin
state - progressively up the superstate chain of
target
- progressively up the superstate chain of
origin
Transitions can therefore be organized in a variety of ways, but ambiguity resolution is regular and predictable, as demonstrated with the Zig
transition in the example below:
The transition lifecycle
A transition performs a stepwise traversal over its domain, which is defined as the subtree rooted at the least common ancestor state between the transition’s source
and target
. At each step in the traversal, the transition instance acts as a temporary substate of the visited state.
The traversal sequence decomposes into an ascending phase, an action phase, and a descending phase.
-
During the ascending phase, the object emits a
depart
event on thesource
, and anexit
event on any state that will be rendered inactive as a consequence of the transition. -
The transition then reaches the domain root and moves into the action phase, whereupon it executes any
action
function defined in its associated transition expression. If anaction
does exist, then the transition remains in the action phase until itsend
method is called. -
Once the transition has
end
ed, it then proceeds with the descending phase, emittingenter
events on any state that is rendered newly active, and concluding with anarrive
event on itstarget
state.
SEE ALSO
Aborted transitions
If a new transition is started while a transition is already in progress, an abort
event is emitted on the previous transition. The new transition will reference the aborted transition as its source
, retaining by reference the same origin
state as that of the aborted transition, and the traversal will resume, starting with a depart
and exit
event emitted on the aborted transition. Further redirections of the pending traversal will continue to grow this source
chain until a transition finally arrives at its target
state.
SEE ALSO
Events
Events in State follow the familiar emitter pattern: State
exposes methods emit
(aliased to trigger
) for emitting typed events, and addEvent
/removeEvent
(aliased to on
/off
and bind
/unbind
) for assigning listeners to a particular event type.
Existential events
Immediately after a state has been fully constructed, it emits a construct
event. Likewise, immediately before a state is cleared from its superstate, or before the owner object’s state implementation is destroyed in the case of a root state, it emits a destroy
event.
Transitional events
During a transition’s traversal from its origin state to its target state, the transition and all affected states along the way emit a sequence of events describing the transition’s progression.
Event sequence
State
: depart — The beginning of the transition consists of exactly one depart
event that is always emitted from the origin state.
Transition
: enter — Next the owner object’s currency is passed from the origin state to the new Transition
, and the transition emits an enter
event.
State
: exit — This is followed by the ascending phase of the transition, which consists of zero or more exit
events, one each from amongst the origin state and any of its superstates that will no longer be active as a result of the transition.
Transition
: start — When the transition reaches the top of its domain, the ascending phase ends and the action phase begins. The transition emits a start
event, and its action function is invoked.
Transition
: end — When the transition’s end
method is called, it emits an end
event, and the descending phase begins.
State
: enter — The descending phase of the transition consists of zero or more enter
events, one for each state that will become newly active.
Transition
: exit — After the transition has enter
ed its target state, the descending phase ends, the transition emits an exit
event, and the object’s currency is passed from the transition to the target state.
State
: arrive — Finally, an arrive
event will occur exactly once, specifically at the target state, marking the end of the transition.
SEE ALSO
Mutation events
When a state’s contents are altered, it emits a mutate
event containing the changes made relative to its immediately prior condition.
Listeners receive four objects as arguments: the contents of the mutation
experienced by the state, a delta
object containing the contents displaced by the mutation, and a full expression of the state’s contents both before
and after
the mutation.
Custom event types
A state’s emit
method allows any type of event to be broadcast and consumed.
Using events to express simple determinism
An event listener may also be expressed as just a state selector, which is interpreted as an order to transition to the indicated state after all of an event’s callbacks have been invoked. This bit of shorthand allows for concise expression of deterministic behavior, where the occurrence of a particular event type within a particular state has a definitive, unambiguous effect on the object’s currency.
Guards
States and transitions can be outfitted with guards that dictate whether and how they may be used.
State guards
For a transition to be allowed to proceed, it must first have satisfied any guards imposed by the states that would be its endpoints: the origin state from which it will depart must agree to release
the object’s currency to the intended target state at which it will arrive, and likewise the target must also agree to admit
the object’s currency from the departed origin.
Here we observe state guards imposing the following restrictions:
-
object
initializes into stateA
, but upon leaving it may never return; we’ve also specifically disallowed direct transitions fromA
toD
. -
State
B
disallows entry from anywhere (for now), and releases conditionally toC
orD
but not directly to any descendant states ofC
; we also note its data itembleep
. -
State
C
imposes no guards, but we note its data itemblorp
. -
State
D
“unlocks”B
; it is also guarded by checking the opposing state’sdata
, allowing admission only from states with a data item keyedblorp
, and releasing only to states with data itembleep
.
The result is that object
is initially constrained to a progression from state A
to C
or its descendant states; exiting the C
domain is initially only possible by transitioning to D
; from D
it can only transition back into C
, however on this and subsequent visits to C
, it has the option of transitioning to either B
or D
, while B
insists on directly returning the object’s state only to one of its siblings C
or D
.
Transition guards
Transition expressions may also include admit
and release
guards. Transition guards are used to decide which one transition amongst possibly several is to be executed as an object changes its state between a given origin
and target
.
About this project
Design goals
Minimal footprint
All functionality of State is to be instigated through the exported state
function. It should be able both to generate state expressions and to implement expressed states into an existing JavaScript object, depending on the arguments provided. In the latter case, the newly implemented system of states should be accessible from a single object.state()
method on the affected object.
Expressive power
As much as possible, State should aim to look and feel like a feature of the language. The interpreted shorthand syntax, simple keyword attributes, and limited interface should allow for production code that is terse, declarative, and easy to write and understand.
Opacity
Apart from the addition of the object.state()
method, a call to state()
must make no other modifications to a State–affected object’s interface. Methods are replaced with delegators, which forward method calls to the current state. This is to be implemented opaquely and non-destructively: consumers of the object need not be aware of which states are active in the object, or even that a concept of state exists at all, and a call to object.state('').destroy()
must restore the object to its original form.
Roadmap
Proposed features
Concurrency
Whereas an object’s state is most typically conceptualized as an exclusive-OR operation (i.e., its current state is always fixed to exactly one state), a state may instead be defined as concurrent, relating its substates in an “AND” composition, where occupation of the concurrent state implies simultaneous occupation of each of its immediate substates.
-
Define a
Region
subclass ofState
that contains the currency-bearing aspect of the currentRootState
; then redefineRootState
as a subclass ofRegion
that contains only the association withowner
and its supplied accessor method. -
Define currency events for
Region
:-
initialize :: ( initialState:State ) ->
-
suspend :: ( currentState:State ) ->
-
resume :: ( currentState:State ) ->
-
conclude :: ( conclusiveState:State ) ->
— Signals entrapment of a region’s currency within a particularState
with attributeconclusive
. Always precedes anyterminate
event. -
terminate :: ( finalState:State ) ->
— Signals termination of a region’s currency. If a currency is terminated imperatively,finalState
may be any intraregionalState
; if terminated naturally,finalState
will be an intraregionalState
with attributefinal
. (A terminal (leaf)conclusive
state may seem implicitlyfinal
, but it is not; a currency in such a state, although trapped there, may be allowed to linger indefinitely before being imperatively terminated.)
-
-
Define region attributes {
permanent
autonomous
volatile
}:-
By default a region is recurrent; i.e. on reactivation of a concurrent superstate, a subregion that has
terminate
d will beinitialize
d with a new currency. Addingpermanent
to a region’s state expression declares that theRegion
will only ever bear one currency, and its finality will persist over the life of theRegion
. -
By default a region can undergo transitions only while its concurrent superstate is active; if deactivated, the region’s currency is
suspend
ed in place, to beresume
d only once the concurrent superstate becomes active again. Theautonomous
attribute allows a region to remain active and continue processing transitions in the background after its concurrent superstate is deactivated. -
By default a region is persistent; it is
suspend
ed in place if deactivated prior to beingfinal
ized, and on subsequent activation will beresume
d from the state that was current at the time it was deactivated. Adding thevolatile
attribute disables this persistence: once deactivated the region’s transition queue is drained and pending transitions are dropped, and on each reactivation the region isinitialize
d anew.
-
-
The destination attributes
initial
,conclusive
, andfinal
only affect their localRegion
. -
Individual transitions are bounded within a single
Region
. A transition arriving at aconcurrent
state constitutes a fork of the currency into the state’s subregions, each of which either spins up a new currency starting from its localinitial
state, orresume
s asuspend
ed currency (for subregions that are recurrent and/or persistent). -
An active subregion’s currency may
terminate
and join the currency of its concurrent superstate, either by arriving at afinal
state, or imperatively. An imperative join may be extrinsic, resulting from the superregion being transitioned away from the concurrent superstate, or intrinsic, resulting from a call toState::join
from within the subregion, which terminates the currency and finalizes it to the current state. -
An object’s “current state” becomes the set of currencies in all active
Region
s; i.e. its state configuration. Representation of the state configuration as a selector string must use nested parens()
or similar to group adjacent regions, delimited by,
,;
, or similar. -
An
owner
’s dispatcher methods are guaranteed to resolve only within the region defined by the root state, and may be stopped at any activeconcurrent
state. Dispatch continues automatically iff the regions are orthogonal, which requires that methods be implemented in no more than one of the subregions. In the ambiguous, non-orthogonal case, dispatch stops descending at the concurrent state, which must contain a spread implementation that “weaves” the dispatch into the multiple regions, and then determines for itself how to reduce the values returned from each region into a single value to be returned to the dispatcher method. A default concurrent method implementation may be made available, which would simply dispatch to each region in a particular order and then return an array of the returned values. -
Resolutions that traverse up a superstate chain are blocked ahead of a concurrent state’s spread implementation, and must stop at the boundary of the subregion.
-
Events may propagate through all subregions unrestricted, as they have no return value.
History
Any state may be directed to keep a history of its own internal state. Entries are recorded in the history anytime the given state is involved in a transition, or experiences a mutation of its internal content or structure. The history may be traversed in either direction, and elements replaced or pushed onto the stack at its current index. When a transition targets a retained state, it will consult that state’s history and redirect itself back to whichever of the state’s substates was most recently current.
Optimization
-
Further granularize the
State::realize
function such that each of the internal closed objects (data
,methods
, etc.), and their associated per-instance methods, would be dynamically added only as needed. -
Allow opt-in to ES5’s meta-programming features and ES6 Proxies on supporting platforms to more deeply embed the state implementation into objects.