State Machines
Wins
- Built-in state machine implementation with nested state machine support.
- Integrated with TIBET signaling system, simplifying conceptual overhead.
- States can receive input data signals to allow inter-state processing.
- Handler-driven transition guards and transition execution functions.
- Integrated with URI Router to manage routes and states consistently.
Contents
Concepts
Cookbook
State Machines
- Creating A State Machine
- Defining A Start State
- Defining A Finish State
- Defining A Standard State
- Defining State Triggers
- Defining Multiple State Triggers
- Defining General Triggers
- Defining Anonymous State Guards
- Defining Machine-Level Guards
- Creating A Nested State Machine
State Responders
State Signals
Code
Concepts
The larger and more complex an application is the more it needs consistent organizing principles and framing that supports them. Organizing how your code reacts to different application states is central to keeping it loosely coupled, modular, and maintainable.
TIBET's TP.core.StateMachine
and TP.core.StateResponder
let you define
application states, their transitions, triggers, and guards in a modular way.
TIBET's state machines can also be nested, letting you factor and reuse common
subcomponent logic.
As a state machine receives input it signals various events such as StateEnter
and StateExit
. The TP.core.StateResponder
trait can be mixed in to any type
to help it process these signals easily and effectively.

For convenience, the TP.core.Controller
type, TP.core.Application
's
supertype, mixes in TP.core.StateResponder
, allowing controllers and the
application instance to function effectively with state machines. TIBET
applications integrate route change notification with matching state names to
synchronize your current route and state.
A viable state machine requires a start state and a final state. Most state machines will include a number of other "normal" states which can cycle during application operation indefinitely. Each potential transition can be guarded implicitly by defining specific trigger requirements for that transition or explicitly via a guard function.
Triggers (TIBET Signals) are observed by TP.core.StateMachine
and used to
cause re-evaluation of the current machine state. If a signal doesn't cause a
transition it is provided as 'state input' allowing state responders to be fed
trigger-related activity.
Signal and state integration also extends to TIBET's routing infrastructure, allowing your application to synchronize its current state with the current client-side route.
Changes in the URL bar (triggered by TP.sys.setRoute
or the user) trigger
activation of the URIRouter. RouteChange
signals are observed by the
TP.core.Application
and used to update the current application state machine
if one is defined.
Other examples of state machines in TIBET can be found in the Lama/TDC keyboard handler logic and in TIBET's drag-and-drop code.
Cookbook
State Machines
State machines in TIBET are configured as a set of current/next state pairs with optional transition details. The overall role of the state machine is to observe input 'triggers' in the form of TIBET signals and to make decisions based on those triggers regarding state transitions. If a trigger results in a state transition the current state is exited and the new state is entered. Signals to this effect are fired to allow state responders to react to these changes. If the trigger does not cause a transition the data is signaled as 'StateInput'.
Creating A State Machine
Creating a state machine instance is a simple one-liner:
machine = TP.core.StateMachine.construct();
A state machine in this form isn't usable yet however. You need to define states so the machine can be activated and begin processing.
State names are normalized to title-case so it's helpful to define your states using the same format to help reinforce that naming convention.
Defining A Start State
To activate
a state machine there must be least one 'start state', a state
whose prior state is null
. Start states are defined with syntax
of the form:
machine.defineState(null, <statename>, [details]);
For now we'll skip the details
and just define a simple start state of 'Home':
machine.defineState(null, 'Home');
With a start state in place the machine can be activated, but it's still not complete, it also needs at least one finish state.
Defining A Finish State
Finish states are states whose 'next state' is null
:
machine.defineState('End');
// OR
machine.defineState('End', null);
Note that transitioning to a final state whose only transition option is null
will automatically cause the state machine to deactivate
. To avoid this you
can ensure that states that are final-able have at least one other potential
transition.
Defining A Standard State
A 'normal' or 'standard' state is simply a state which is neither a start nor final state. Normal state definitions consist of a prior state and next state pair and associated transition details, if any:
machine.defineState('Start', 'Left');
machine.defineState('Left', 'End');
machine.defineState('Start', 'Right');
machine.defineState('Right', 'End');
When combined with our prior 'Start' and 'End' state definitions we've now defined a simple state machine of the form:
--- Left ---
/ \
null -- Start End -- null
\ /
--- Right --
Prior to activate
the machine's state is null
. Once we activate
the
machine will have a state of Start
. Triggers, which we haven't yet defined,
cause the state machine to evaluate whether it can transition to Left
or
Right
(the two states which have Start
as a prior state). If the state
machine transitions Left
or Right
it can then eventually transition to
End
, which will cause it to perform any final processing and deactivate
since the End
state has no target states other than null
.
As it turns out, this state machine isn't valid since there's no logic provided
to help it determine when to go Left
and when to go Right
. We can resolve
that problem by defining transition details that include either unique triggers
or guards.
Defining State Triggers
A state trigger is an origin/signal pair which causes the state machine to see if it can transition from the current state to a potential target state.
In the example below we've defined Left
and Right
states but also assigned
them transition details which include trigger signal names:
machine.defineState('Start', 'Left', {trigger: 'GoLeft'});
machine.defineState('Left', 'End', {trigger: 'AllDone'});
machine.defineState('Start', 'Right', {trigger: 'GoRight'});
machine.defineState('Right', 'End', {trigger: 'AllDone'});
With the definitions above we've now told our state machine that once we're in
the Start
state we go Left
if the triggering signal is a 'GoLeft' from any
origin. Likewise we've told it we go Right
if we see a 'GoRight' signal.
Triggers are officially always a combination of origin
and signal
in TIBET
terms. If you define the trigger
as a string it is assumed to be the signal
name and the origin defaults to TP.ANY
. You can use an array to define the
pair explicitly:
machine.defineState('Start', 'Left',
{trigger: TP.ac(TP.sys.getApplication(), 'GoLeft')});
Defining Multiple State Triggers
You can define multiple triggers using the triggers
key as shown below.
NOTE that when using multiple triggers you must provide each item as an
origin/signal pair:
machine.defineState('Start', 'Left',
{triggers: [
TP.ac(TP.sys.getApplication(), 'GoLeft'),
TP.ac(TP.ANY, 'LeftIsBest'),
]});
Defining General Triggers
In addition to providing state-specific triggers you can also assign one or more triggers to the state machine itself. Such triggers can be used in conjuction with guard function logic to perform transition filtering, or they can be used as a way of feeding particular states with input data.
You define a state-machine level trigger via the addTrigger
and addTriggers
calls respectively:
machine.addTrigger(TP.ANY, 'ItHappened');
machine.addTriggers(TP.ac(TP.ANY, 'ItHappened'), TP.ac(TP.ANY, 'AndSoDidThis'));
When a state machine is triggered but the trigger does not result in a
transition event the trigger is used as StateInput
. An example is the TIBET
drag-and-drop code which uses mouse down events to activate dragging and mouse
up events to deactivate it, but which also uses mouse move events while in the
dragging state as input data.
Defining Anonymous State Guards
When you need more logic to determine whether a particular transition should
occur you can define a guard function. This can be done as part of the state's
transition details by assigning a guard function to the guard
key in the
transition details:
machine.defineState('Start', 'Left',
{ guard:
function(trigger) {
return TP.sys.getRoute() === 'Left';
}
});
In the sample above we've defined our Start
-to-Left
state transition as
being guarded by a function that will only go Left
if the current route is set
to Left.
Note that guard functions are passed the signal or dictionary which contains any triggering details related to the potential transition.
Defining Machine-Level Guards
Guard functions can be defined locally on the state machine using
defineMethod
.
When implementing a guard function use a method name with accept
and the state
in question. For example, acceptLeft
would guard transitions to Left
:
machine.defineMethod('acceptLeft', function(trigger) {
var shouldAccept;
// Some logic which assigns true or false to 'shouldAccept'
...
return shouldAccept;
});
Note that because we're invoking defineMethod
on an instance of state machine
the guard function is "local" to that machine and not used by other instances.
Creating A Nested State Machine
State machines can be nested by defining a nested
property in the state
definition details and providing the child state machine. Using this approach
you essentially define a parent state as being managed entirely by a child state
machine. When you enter the parent state the child state machine is activated.
When the child finalizes/deactivates the parent state will attempt to
transition to one of its target states.
// Define a child state machine with appropriate transition details etc.
child = TP.core.StateMachine.construct();
child.defineState(null, 'ChildStart');
child.defineState('ChildStart', 'Active', {trigger: 'ChildGo'});
child.defineState('Active', 'ChildEnd', {trigger: 'ChildStop'});
child.defineState('ChildEnd');
// Define a parent state machine whose 'Active' state is managed entirely by
// the child state machine.
parent = TP.core.StateMachine.construct();
parent.defineState(null, 'ParentStart');
parent.defineState('ParentStart', 'Active', {nested: child});
parent.defineState('Active', 'ParentEnd');
parent.defineState('ParentEnd');
Child/nested state machines work just like any other state machine during normal operation. Their primary difference is their behavior regarding bubbling unhandled events up to their parent and their notification of the parent when they finish/deactivate.
State Responders
State responders in TIBET are simply objects with convenience methods which let them easily observe and ignore one or more state machines.
The primary activity of a state machine in TIBET is to process triggers and determine which, if any, state transition or state input signals should be fired in response. State responders are designed to make observing and handling these state signals easy.
Defining State Responder Types
TP.core.StateResponder
is a type intended to be mixed in to other
types using TIBET's addTraits
machinery. Using a trait allows any type to be a
state responder.
Use TIBET's addTraits
syntax to create responder-enabled types/instances:
// Assume APP.xyz.StateAware needs to be a state responder:
APP.xyz.StateAware.addTraits(TP.core.StateResponder);
TIBET's controller types (which include TP.core.Application
) mix in
StateResponder
making it easy to respond to state signals in application
controllers.
Once you've added TP.core.StateResponder
to a type all that remains is to
define the specific state signal handlers. See the section on State Signals for specific details on the various signal
types and sequencing of TIBET's state signals.
Constructing A Responder Instance
Once you have a responder-enabled type defined you use construct
just as you
normally would, then associate the responder with a TP.core.StateMachine
:
inst = APP.xyz.StateAware.construct();
// Assume 'machine' has been previously defined. Associate the
// responder-aware instance with at least one state machine:
inst.addStateMachine(machine);
// Ensure the state machine gets activated at some point. Once this
// is done the machine will begin processing triggers/firing state signals:
machine.activate();
Associating the state responder with a state machine will automatically cause the state responder to begin observing signals from the state machine.
Note that the state machine must receive an activate
call to begin processing
its triggers and sending out state signals to any responders.
State Signals
State machines observe TIBET signals as triggers and fire a variety of signals themselves to notify potential responders of state-related events.
Inbound triggering signals can be of any type and originate from any origin. The state machine can integrate with any aspect of your application through these triggers.
Outbound state signals originate from the state machine and are subtypes of
TP.sig.StateSignal
or one of its more specific subtypes:
TP.sig.StateEnter
, TP.sig.StateExit
, TP.sig.StateTransition
, and
TP.sig.StateInput
.
To respond to outbound state signals your state responders should implement
appropriate handlers using TIBET's defineHandler
syntax.
Code
The TIBET kernel contains the main code for state machine support in:
~lib/src/tibet/kernel/TIBETStateMachineTypes.js
Additional code that drives how TIBET's state machine logic functions with respect to signaling can be found in:
~lib/src/tibet/kernel/TIBETNotification.js