TIBET Logo

TIBET State Management

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
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.


TIBET State Machine Overview

TIBET State Machine Overview


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 Sherpa/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