Installation

state.js 0.0.8 (147kB)9.3kB min/gz


The lone dependency of State is Omicron, a small set of object-focused utility functions for performing differential operations, facilitating inheritance, and other common tasks.

omicron.js 0.1.9 (25kB)2.5kB min/gz


In node.js

State can be installed as a node.js module via npm:

$ npm install state
var state = require('state');
state = require 'state'

In the browser

State can be included using your favorite package manager, or directly:

<script src="omicron.js"></script>
<script src="state.js"></script>

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:

( function () {
    var state = window.state.noConflict();
    // ...
}() );
state = window.state.noConflict()
# ...
⏎  Installation

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
state( owner, [attributes], expression )
state( owner, [attributes], expression )

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
state( [attributes], expression )
state( [attributes], expression )

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:

var person = {
    greet: function () { return "Hello."; }
};

state( person, {
    Formal: {
        greet: function () { return "How do you do?"; }
    },
    Casual: {
        greet: function () { return "Hi!"; }
    }
});
person =
  greet: -> "Hello."

state person,
  Formal:
    greet: -> "How do you do?"
  Casual:
    greet: -> "Hi!"

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:

person.state();  // >>> State ''
person.state()  # >>> 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:

person.greet();  // >>> "Hello."
person.greet()  # >>> "Hello."

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:

person.state().change('Formal');
person.state();                   // >>> State 'Formal'
person.greet();                   // >>> "How do you do?"
person.state().change 'Formal'
person.state()                  # >>> State 'Formal'
person.greet()                  # >>> "How do you do?"

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:

person.state('-> Casual');
person.state();                   // State 'Casual'
person.greet();                   // >>> "Hi!"

person.state('->');                                           // [1]
person.state();                   // State ''
person.greet();                   // >>> "Hello."
person.state '-> Casual'
person.state()                  # >>> State 'Casual'
person.greet()                  # >>> "Hi!"

person.state '->'                                             # [1]
person.state()                  # >>> State ''
person.greet()                  # >>> "Hello."
  1. 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.

  • 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 formal StateExpression, which is then used to create a State instance.

  • Inheritance — An owner object’s states are arranged hierarchically in a rooted tree structure: the owner is given exactly one root state, within which may be nested zero or more substates, which may themselves contain further substates, and so on, thereby expressing specificity of the owner’s behavior. A state inherits from its superstate, with which it shares the same owner, and also inherits from any protostate, defined as the equivalently positioned state within a prototype of the owner object. Protostates have a higher inheriting precedence than superstates.

  • Selectors — A stateful owner object’s accessor method at object.state() can be called without arguments to retrieve the object’s current state, or, if provided a selector string, to query for a specific State of the object, or a specific set of states.

  • 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 the State that the expression is to represent.

  • Data — Arbitrary data can be attached to each state, and inherited accordingly through protostates and superstates.

  • Methods — Behavior is modeled by defining state methods that override the object’s methods. Consumers of the object simply call its methods as usual, and need not be aware of the object’s current state, or even that a concept of state exists at all. State methods are invoked in the context of the state in which the method is defined, and may also be transformed to include further bindings to its state–lexical environment.

  • 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 — Listeners for specific event types can be bound to a state, which will be called in the context of the bound state as it is affected by a progressing transition, as the state itself experiences changes to its content, or upon the state’s construction or destruction. State also allows for 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.

⏎  Overview

Concepts

States

Formally, a state is an instance of State that encapsulates all or part of an owner object’s condition at a given moment. A stateful owner is able to adopt a distinct set of behaviors for itself by occupying a particular state as its current state, during which some of its methods will exhibit the behaviors described by that state.

A stateful owner usually bears multiple states, and as such it is able to alter its behavior in a definitive manner by instigating a transition, which moves the owner’s currency from its previously current state to whichever state is targeted by the transition.

⏎  States  <  Concepts  <  Overview

Expressions

A state expression is a data structure that represents the real or intended semantic content for a State instance.

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 categories: data, methods, events, guards, states, and transitions, along with the encoded attributes. 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 formal StateExpression.

express state() StateExpression

Structured state expressions

Building upon the introductory example, we could write a state expression that consists of explicitly categorized states, methods, and events, etc., looking something like this:

var longformExpression = state({
    methods: {
        greet: function () { return "Hello."; }
    },
    states: {
        Formal: {
            methods: {
                greet: function () { return "How do you do?"; }
            },
            events: {
                enter: function () { this.owner().wearTux(); }
            }
        },
        Casual: {
            methods: {
                greet: function () { return "Hi!"; }
            },
            events: {
                enter: function () { this.owner().wearJeans(); }
            }
        }
    }
});
// >>> StateExpression
longformExpression = state
  methods:
    greet: -> "Hello."
  states:
    Formal:
      methods:
        greet: -> "How do you do?"
      events:
        enter: -> do @owner().wearTux
    Casual:
      methods:
        greet: -> "Hi!"
      events:
        enter: -> do @owner().wearJeans
# >>> StateExpression

Shorthand

Explicit categorization 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:

var shorthandExpression = state({
    greet: function () { return "Hello."; },

    Formal: {
        enter: function () { this.owner().wearTux(); },
        greet: function () { return "How do you do?"; }
    },
    Casual: {
        enter: function () { this.owner().wearJeans(); },
        greet: function () { return "Hi!"; }
    }
});
// >>> StateExpression
shorthandExpression = state
  greet: -> "Hello."
  Formal:
    enter: -> do @owner().wearTux
    greet: -> "How do you do?"
  Casual:
    enter: -> do @owner().wearJeans
    greet: -> "Hi!"
# >>> StateExpression

Interpreting the input supplied in this example, the state() invocation:

  • recognized the absence of any items whose keys are category names, and instead inferred that object literals Formal and Casual describe states.

  • identified enter as a built-in event type and thus treated the associated function values as listeners for enter events that will be emitted by the containing state.

  • inferred that functions keyed greet, which is not a built-in event type, were to be treated as a method of the containing state.

Explicit categorization 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:

  1. If an entry’s value is a typed StateExpression or TransitionExpression, interpret it as-is, using the entry’s key as its name, or, if the entry’s value is the exported state function itself, interpret it as an empty state whose name is the entry’s key.

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

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

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

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

StateExpression interpret

⏎  Expressions  <  Concepts  <  Overview

Inheritance

The State object model is a classic rooted tree structure. Any state may serve as a superstate of one or more substates, each of which expresses further specificity of their common owner object’s behavior and condition.

An owner object’s state tree is further heritable by any prototypal inheritors of that object, which view their prototype’s states as protostates, from which their own states, as epistates, may inherit.

The root state

For every stateful object, a single root state is automatically generated, which is the top-level superstate of all other states. The root state’s name is always and uniquely the empty string ''. Either an empty-string selector or naked transition arrow may be used to change an object’s current state to the root state, causing the object to exhibit its default behavior.

obj.state().root() === obj.state('');   // >>> true
obj.state('->');
obj.state();                            // >>> State ''
obj.state().root() is obj.state ''      # >>> true
obj.state '->'
obj.state()                             # >>> State ''

The root state also acts as the default method store for the object’s state implementation, containing any methods originally defined on the object itself, for which now exist one or more stateful reimplementations elsewhere within the state tree.

This is the basis for State’s method delegation pattern, wherein a method call made on the object is automatically forwarded to the object’s current state, with the assurance that the call will be resolved somewhere in the state tree. If a method override is not present on the current state, then the call is forwarded on to its superstate, and so on as necessary, until as a last resort State will resolve the call using the object’s original implementation of the method, held within the root state.

See also: Delegator methods

root State::root

Superstates and substates: nesting specific behavior

Substates help to express ever greater specificity of their owner’s behavior and condition. An object may exhibit a specific condition by transitioning to a state nested deep within the tree, and it is also free to express itself more generically by transitioning to a concrete interior state.

function Person () {
    this.give = function ( to, what ) {
        to.receive( this, what );
        return this;
    };
    this.receive = function ( from, what ) { return this; };
    
    this.greet = function () { return "Hello."; };
    
    state( this, {
        Formal: {
            greet: function ( person ) { return "How do you do?"; }
        },
        Informal: {
            greet: function ( person ) { return "Hi!"; },

            Familiar: {
                hug: function ( person ) {
                    this.owner().give( person, 'O' );
                },

                greet: function ( person ) {
                    this.owner().hug( person );
                },

                Intimate: {
                    kiss: function ( person ) {
                        this.owner().give( person, 'X' );
                    },

                    greet: function ( person ) {
                        this.superstate().call( 'greet', person );
                        this.owner().kiss( person );
                    }
                }
            }
        }
    });
}
class Person
  constructor: ->
    @give = ( to, what ) -> to.receive this, what; this
    @receive = ( from, what ) -> this
    @greet = -> "Hello."

    state this,
      Formal:
        greet: ( person ) -> "How do you do?"
      
      Informal:
        greet: ( person ) -> "Hi!"
    
        Familiar:
          hug: ( person ) -> @owner().give person, 'O'
          greet: ( person ) -> @owner().hug person
    
          Intimate:
            kiss: ( person ) -> @owner().give person, 'X'
            greet: ( person ) ->
              @superstate().call 'greet', person
              @owner().kiss person

Protostates: inheriting states across prototypes

All of the examples given to this point have created stateful objects by applying the state() function directly to the object. Consider now the case of an object that inherits from a stateful prototype.

function Person () {}
Person.prototype.greet = function () { return "Hello."; };
state( Person.prototype, {
    Formal: {
        greet: function () { return "How do you do?"; }
    },
    Casual: {
        greet: function () { return "Hi!"; }
    }
});

var person = new Person;
'state' in person;                 // >>> true
person.hasOwnProperty('state');    // >>> false
class Person
  greet: -> "Hello."
  state @::
    Formal:
      greet: -> "How do you do?"
    Casual:
      greet: -> "Hi!"

person = new Person
'state' of person                  # >>> true
person.hasOwnProperty 'state'      # >>> false

At this point person inherits the state method from its prototype, as it lacks a state implementation of its own. However, calling person.state() automatically creates a new state implementation for person.

person.state();                    // >>> State ''
person.hasOwnProperty('state');    // >>> true
person.state()                     # >>> State ''
person.hasOwnProperty 'state'      # >>> true

Now, person will automatically inherit all content from the states of its prototype, which it identifies as its protostates. It will maintain its own currency and transitions over the inherited protostates, leaving the currency of the prototype unaffected.

person.state().isVirtual();        // >>> false
person.greet();                    // >>> "Hello."

person.state('-> Casual');
person.state();                    // >>> State 'Casual'
person.state().isVirtual();        // >>> true
person.greet();                    // >>> "Hi!"

Person.prototype.state();          // >>> State ''
person.state().isVirtual()         # >>> false
person.greet()                     # >>> "Hello."

person.state '-> Casual'
person.state()                     # >>> State 'Casual'
person.state().isVirtual()         # >>> true
person.greet()                     # >>> "Hi!"

Person::state()                    # >>> State ''
Under the hood

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.

The inheritor may adopt a protostate as its current state just as it would with a state of its own. When that happens, a temporary, lightweight virtual state is created within the state implementation of the inheritor, acting as a stand-in for the protostate. Virtual states exist only so long as they are active; once the object transitions elsewhere, any virtual states consequently rendered inactive are automatically destroyed.

This system of protostates and virtual states allows an object’s state implementation to benefit from the prototypal reuse patterns of JavaScript without entangling the constituent State instances themselves in any direct prototypal relationships with each other.

createAccessor

protostate isProtostateOf State constructor State::protostate

⏎  Inheritance  <  Concepts  <  Overview

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.

var o = {};
state( o, {
    A: {
        AA: state( 'initial', {
            AAA: state
        }),
        AB: state
    },
    B: state
});

o.state();            // >>> State 'AA'
o.state('');          // >>> State ''
o.state('A.AA.AAA');  // >>> State 'AAA'
o.state('.');         // >>> State 'AA'
o.state('..');        // >>> State 'A'
o.state('...');       // >>> State ''
o.state('.AAA');      // >>> State 'AAA'
o.state('..AB');      // >>> State 'AB'
o.state('...B');      // >>> State 'B'
o.state('AAA');       // >>> State 'AAA'
o.state('.*');        // >>> [ State 'AAA' ]
o.state('AAA.*');     // >>> []
o.state('*');         // >>> [ State 'A', State 'B' ]
o.state('**');        // >>> [ State 'A', State 'AA', State 'AAA',
                      //       State 'AB', State 'B' ]
o = {}
state o,
  A:
    AA: state 'initial'
      AAA: state
    AB: state
  B: state

o.state()             # >>> State 'AA'
o.state ''            # >>> State ''
o.state 'A.AA.AAA'    # >>> State 'AAA'
o.state '.'           # >>> State 'AA'
o.state '..'          # >>> State 'A'
o.state '...'         # >>> State ''
o.state '.AAA'        # >>> State 'AAA'
o.state '..AB'        # >>> State 'AB'
o.state '...B'        # >>> State 'B'
o.state 'AAA'         # >>> State 'AAA'
o.state '.*'          # >>> [ State 'AAA' ]
o.state 'AAA.*'       # >>> []
o.state '*'           # >>> [ State 'A', State 'B' ]
o.state '**'          # >>> [ State 'A', State 'AA', State 'AAA',
                      #       State 'AB', State 'B' ]

State uses a simple selector format:

  1. State names are delimited from their member substates with the dot (.) character.

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

  3. A fully-qualified name is not necessary except for disambiguation: 'A.B.C' and 'C' will both resolve to the deep substate named C provided that there is no other state named C located higher in the state tree.

  4. Special cases: empty-string '' references the root state; single-dot . references the local context state; double-dot .. references its immediate superstate, etc.

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

query $ State::query

⏎  Selectors  <  Concepts  <  Overview

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.

state( obj, 'abstract', {
    Alive: state( 'default initial mutable', {
        update: function () { /*...*/ }
    }),
    Dead: state( 'final', {
        update: function () { /*...*/ }
    })
});
state obj, 'abstract'
  Alive: state 'default initial mutable'
    update: -> # ...
  Dead: state 'final'
    update: -> # ...

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.

Attributes state/attributes State.privileged.attributes

Mutability attributes

mutable finite immutable

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

abstract concrete default

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

initial conclusive final

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.

⏎  Attributes  <  Concepts  <  Overview

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.

function Chief () {
    state( this, 'mutable', {
        Enraged: {
            Thermonuclear: {
                data: {
                    mission: 'destroy'
                    budget: Infinity
                }
            }
        }
    });
}
state( Chief.prototype, {
    data: {
        mission: 'innovate',
        budget: 10000000000
    },
    Enraged: {
        data: {
            action: 'compete'
        }
    }
}


var mobs = new Chief;
mobs.state().data();
// >>> { mission: 'innovate', budget: 10000000000 }

mobs.state('-> Enraged');
mobs.state().data({ target: 'Moogle' });
mobs.state().data();
// >>> { target: 'Moogle', mission: 'compete', budget: 10000000000 }

mobs.state().go('Thermonuclear');
mobs.state().data();
// >>> { target: 'Moogle', mission: 'destroy', budget: Infinity }
class Chief
  state @::
    data:
      mission: 'innovate'
      budget: 10000000000
    Enraged:
      data:
        mission: 'compete'

  constructor: ->
    state this, 'mutable'
      Enraged:
        Thermonuclear:
          data:
            mission: 'destroy'
            budget: Infinity


mobs = new Chief
mobs.state().data()
# >>> { mission: 'innovate', budget: 10000000000 }

mobs.state '-> Enraged'
mobs.state().data target: 'Moogle'
mobs.state().data()
# >>> { target: 'Moogle', mission: 'compete', budget: 10000000000 }

mobs.state().go 'Thermonuclear'
mobs.state().data()
# >>> { target: 'Moogle', mission: 'destroy', budget: Infinity }

data has get let set delete

State.privileged.data

⏎  Data  <  Concepts  <  Overview

Methods

A defining feature of State is the ability for an object to exhibit a variety of behaviors. A state expresses a particular behavior with state methods that override any of its owner object’s methods.

Delegators

When state is applied to an object by calling state(), State first identifies any methods already present on the object for which there exists at least one override somewhere within the provided state expression. These methods will be relocated to the new root state, and replaced on the object with a special delegator method.

The delegator’s job is to take any subsequent calls it receives and redirect them to the object’s current state, from which State will then locate and invoke the proper stateful implementation of the method. Should no active states contain an override for the invoked method, the delegation will default to the object’s original implementation of the method if one exists, or result in a noSuchMethod event otherwise.

function shoot () { return "pew!"; }
var raygun = { shoot: shoot };

// First, affirm the identity of the function serving as the `shoot`
// method of `raygun`.
raygun.shoot === shoot;                     // >>> true

// Make `raygun` stateful. Its initial state will be the root state.
state( raygun, {
    RapidFire: {
        shoot: function () {
            return "pew pew pew!";
        }
    }
});

// Demonstrate that the function serving as the `shoot` method of
// `raygun` has been changed, that it has been replaced with a new
// delegator method, and that the original has been moved to the
// root state and, since `raygun` is presently in that state, can be
// be called in the normal fashion.
raygun.shoot === shoot;                     // >>> false
raygun.shoot.isDelegator;                   // >>> true
raygun.state('').method('shoot') === shoot; // >>> true
raygun.shoot();                             // >>> "pew!"

// Change `raygun` to the `RapidFire` state and demonstrate the
// difference in behavior.
raygun.state('-> RapidFire');
raygun.state();                             // >>> State 'RapidFire'
raygun.shoot();                             // >>> "pew pew pew!"
shoot = -> "pew!"
raygun = shoot: shoot

# First, affirm the identity of the function serving as the `shoot`
# method of `raygun`.
raygun.shoot is shoot                       # >>> true

# Make `raygun` stateful. Its initial state will be the root state.
state raygun,
  RapidFire:
    shoot: -> "pew pew pew!"

# Demonstrate that the function serving as the `shoot` method of
# `raygun` has been changed, that it has been replaced with a new
# delegator method, and that the original has been moved to the
# root state and, since `raygun` is presently in that state, can be
# called in the normal fashion.
raygun.shoot is shoot                       # >>> false
raygun.shoot.isDelegator                    # >>> true
raygun.state('').method('shoot') is shoot   # >>> true
raygun.shoot()                              # >>> "pew!"

# Change `raygun` to the `RapidFire` state and demonstrate the
# difference in behavior.
raygun.state '-> RapidFire'
raygun.state()                              # >>> State 'RapidFire'
raygun.shoot()                              # >>> "pew pew pew!"

State createDelegator State.privileged.addMethod

Method context

When a call to an owner’s method is delegated to a state method, that state method is invoked not in the context of the owner, but rather of the State in which it is declared, or, if the method is inherited from a protostate, in the context of the owner’s state which inherits from that protostate. Using the state as the method’s context does mean that, within a state method, while the owner is not referenced by this as it normally would be, it is still always accessible by calling this.owner().

This policy of binding state methods to their associated State affords them the important advantage of being able to express certain lexical idioms, such as calling up to a superstate’s implementation of a method, as facilitated by the apply and call methods of State.

state( owner, {
    A: {
        bang: function ( arg1, arg2 ) { /* ... */ },
        B: {
            bang: function () {
                return this.superstate().apply( 'bang', arguments );
            }
        }
    }
});
state owner,
  A:
    bang: ( arg1, arg2 ) -> # ...
    B:
      bang: -> @superstate().apply 'bang', arguments

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. (Supplying a context object is unnecessary as a consequence of state methods being automatically bound to their State.)

apply call method

State::apply State::call State.privileged.method

Optional lexical bindings

Wrapping a method’s declaration in a call to state.method embeds additional contextual variables into a method, which are particularly useful when a method needs to inherit behavior from a protostate.

var q = {
  foo: "FOO", bar: "BAR",
  m: function () { return this.foo; }
};
state( q, {
  A: {
    m: state.method( function () {
      return superstate.call('m') + owner.bar;
    }),
    AA: state
  }
});

var p = Object.create( q, {
  baz: { value: "BAZ" }
});
state( p, {
  A: {
    m: state.method( function () {
      return protostate.call('m') + owner.baz;
    })
  }
});

var o = Object.create( p );
o.m();              // >>> "FOO"

o.state('-> AA');
o.m();              // >>> "FOOBARBAZ"
class Superclass
  foo: "FOO", bar: "BAR"
  m: -> @foo
  state @::
    A:
      m: state.method -> superstate.call('m') + owner.bar
      AA: state

class Class extends Superclass
  baz: "BAZ"
  state @::
    A:
      m: state.method -> protostate.call('m') + owner.baz

o = new Class
o.m()             # >>> "FOO"
o.state '-> AA'    
o.m()             # >>> "FOOBARBAZ"

Methods transformed in this way gain four state–lexical variable bindings that can be referenced directly from within the function body:

  • autostate : the precise State in which the method is defined.
  • protostate : the protostate of autostate.
  • superstate : the superstate of this.
  • owner : the owner of this.

Worth noting here is the distinction and relationship between autostate and this: if a method is inherited from a protostate, then autostate will reference that protostate of this; if the method is not inherited, then autostate and this are identical.

state.method state.method

Lexical bindings in state methods

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.

var log = function ( msg ) { return console.log( msg ); },
    owner = {},
    root;

state( owner, 'abstract', {
    foo: function () { log("I exist!"); },

    A: state( 'default', {
        bar: function () { log("So do I!"); }
    }),
    B: state
});
// >>> State 'A'

root = owner.state('');
root.on( 'noSuchMethod', function ( methodName, args ) {
    log("`owner` has no method " + methodName + " in this state!");
});
root.on( 'noSuchMethod:bar': function () {
    log("Here’s another way to trap a bad call to 'bar'.");
});

owner.foo();            // log <<< "I exist!"
owner.bar();            // log <<< "So do I!"
owner.state('-> B');
owner.state();          // >>> State 'B'
owner.foo();            // log <<< "I exist!"
owner.bar();            // >>> undefined
// log <<< "`owner` has no method 'bar' in this state!"
// log <<< "Here’s another way to trap a bad call to 'bar'."
log = ( msg ) -> console.log msg
owner = {}

state owner, 'abstract'
  foo: -> log "I exist!"

  A: state 'default'
    bar: -> log "So do I!"
  B: state
# >>> State 'A'

root = owner.state ''
root.on 'noSuchMethod', ( methodName, args ) ->
  log "`owner` has no method '#{methodName}' in this state!"
root.on 'noSuchMethod:bar', ( args... ) ->
  log "Here’s another way to trap a bad call to 'bar'."

owner.foo()             # log <<< "I exist!"
owner.bar()             # log <<< "So do I!"
owner.state '-> B'
owner.state()           # >>> State 'B'
owner.foo()             # log <<< "I exist!"
owner.bar()             # >>> undefined
# log <<< "`owner` has no method 'bar' in this state!"
# log <<< "Here’s another way to trap a bad call to 'bar'."

noSuchMethod noSuchMethod:name apply State::apply

Example

This example of a simple Document type demonstrates some of the patterns of state method inheritance. Note the points of interest numbered in the trailing comments and their explanations below:

var fs = require('fs'),
    state = require('state');

function Document ( location, text ) {
    this.location = function () {
        return location;
    };
    this.read = function () {
        return text;
    };
    this.edit = function ( newText ) {                      // [1]
        text = newText;
        return this;
    };
}
state( Document.prototype, 'abstract', {
    freeze: function () {                                   // [3]
        var result = this.call('save');                     // [4]
        this.change('Frozen');
        return result;
    },

    Dirty: {
        save: function () {
            var owner = this.owner(),
                args = [ owner.location(), owner.read() ];
            this.change( 'Saved', args );                   // [5]
            return owner;
        }
    },
    Saved: state( 'initial', {
        edit: function () {
            var ss = this.superstate(),
                result = ss.apply( 'edit', arguments );     // [2]
            this.change('Dirty');
            return result;
        },

        Frozen: state( 'final', {
            edit: function () {},
            freeze: function () {}
        })
    }),

    transitions: {
        Writing: {
            origin: 'Dirty', target: 'Saved',
            action: function ( location, text ) {
                var transition = this;
                
                function cb ( err ) {
                    if ( err ) {
                        return transition.abort( err )
                                         .change('Dirty');
                    }
                    transition.end();
                }

                return fs.writeFile( location, text, cb );
            }
        }
    }
});
fs = require 'fs'
state = require 'state'

class Document
  constructor: ( location, text ) ->
    @location = -> location
    @read = -> text
    @edit = ( newText ) ->                                  # [1]
      text = newText
      this

  state @::, 'abstract'
    freeze: ->                                              # [3]
      result = @call 'save'                                 # [4]
      @change 'Frozen'
      result

    Dirty:
      save: ->
        owner = @owner()
        args = [ owner.location(), owner.read() ]
        @change 'Saved', args                               # [5]
        owner
    
    Saved: state 'initial'
      edit: ->
        result = @superstate().apply 'edit', arguments      # [2]
        @change 'Dirty'
        result

      Frozen: state 'final'
        edit: ->
        freeze: ->

    transitions:
      Writing:
        origin: 'Dirty', target: 'Saved'
        action: ( location, text ) ->
          fs.writeFile location, text, ( err ) =>
            return @abort( err ).change('Dirty') and this if err
            do @end
  1. A “privileged” method edit is defined inside the constructor, closing over a private variable text to which it requires access. Later, when state is applied to the object, this method will be moved to the root state, and a delegator will be added to the object in its place.

  2. The edit method override defined on the Saved state is not closed over the constructor’s private variable text, but it can still set the value of text by using this.superstate().apply to invoke the overridden original implementation.

  3. The freeze method is declared on the abstract root state, callable from states Dirty and Saved (but not Frozen, where it is overridden with a no-op).

  4. The save method, which only appears in the Dirty state, is still callable from other states, as its presence in Dirty causes a no-op version of the method to be automatically added to the root state. This allows freeze to safely call save despite the possiblity of being in a state (Saved) with no such method.

  5. Changing to Saved from Dirty results in the Writing transition, whose asynchronous action is invoked with the arguments array provided by the change call.

⏎  Methods  <  Concepts  <  Overview

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:

  1. at the expression’s target state (compare to the manner in which CSS3 transitions are declared with respect to classes)
  2. at the expression’s origin state
  3. progressively up the superstate chain of target
  4. 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:

// An asynchronous logger
function log ( message, callback ) { /* ... */ }

function Foo () {}
state( Foo.prototype, 'abstract', {
    Bar: state('default initial'),
    Baz: {
        transitions: {
            Zig: { action: function () {
                var transition = this;
                log( "BLEEP", function () { transition.end(); } );
            } }
        }
    },

    transitions: {
        Zig: { origin: 'Bar', target: 'Baz', action: function () {
            var transition = this;
            log( "bleep", function () { transition.end(); } );
        } },
        Zag: { origin: 'Baz', target: 'Bar', action: function () {
            var transition = this;
            log( "blorp", function () { transition.end(); } );
        } }
    }
});

var foo = new Foo;

function zig () {
    var transition;
    foo.state();                   // >>> State 'Bar'
    foo.state('-> Baz');           // (enacts `Zig` of `Baz`)
    transition = foo.state();      // >>> Transition 'Zig'
    transition.on( 'end', zag );
}

function zag () {
    var transition;
    foo.state();                   // >>> State 'Baz'
    foo.state('-> Bar');           // (enacts `Zag` of root state)
    transition = foo.state();      // >>> Transition `Zag`
    transition.on( 'end', stop );
}

function stop () {
    return "take a bow";
}

zig();
// ...
// log <<< "BLEEP"
// ...
// log <<< "blorp"
# An asynchronous logger
log = ( message, callback ) -> # ...

class Foo
  state @::, 'abstract'
    Bar: state 'default initial'
    Baz:
      transitions:
        Zig: action: ->
          log "BLEEP", => @end()

    transitions:
      Zig: origin: 'Bar', target: 'Baz', action: ->
        log "bleep", => @end()
      Zag: origin: 'Baz', target: 'Bar', action: ->
        log "blorp", => @end()

foo = new Foo

zig = ->
  foo.state()               # >>> State 'Bar'
  foo.state '-> Baz'        # (enacts `Zig` of `Baz`)
  transition = foo.state()  # >>> Transition 'Zig'
  transition.on 'end', zag

zag = ->
  foo.state()               # >>> State 'Baz'
  foo.state '-> Bar'        # (enacts `Zag` of root state)
  transition = foo.state()  # >>> Transition 'Zag'
  transition.on 'end', stop

stop = -> "take a bow"

do zig
# ...
# log <<< "BLEEP"
# ...
# log <<< "blorp"

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.

  1. During the ascending phase, the object emits a depart event on the source, and an exit event on any state that will be rendered inactive as a consequence of the transition.

  2. 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 an action does exist, then the transition remains in the action phase until its end method is called.

  3. Once the transition has ended, it then proceeds with the descending phase, emitting enter events on any state that is rendered newly active, and concluding with an arrive event on its target state.

function Mover () {}
state( Mover.prototype, {
    
    // Programmatically set up the root to log the transitional
    // events of all states
    construct: function () {
        var events, substates, i, j;
        events = ['depart', 'exit', 'enter', 'arrive'];
        substates = [this].concat( this.substates( true ) );
        for ( i in substates ) for ( j in events ) {
            ( function ( s, e ) {
                s.on( e, function () {
                    console.log this.name() + " " + e;
                });
            }( substates[i], events[j] ) );
        }
    },

    Stationary: {
        Idle: state('initial'),
        Alert: state
    },
    Moving: {
        Walking: state,
        Running: {
            Sprinting: state
        }
    },

    transitions: {
        Announcing: {
            source: '*', target: '*',
            action: function () {
                var name = this.superstate().name() || "the root";
                this.end( "action of transition is at " + name );
            },
            end: function ( message ) { console.log( message ); }
        }
    }
});


var m = new Mover;

m.state('-> Alert');
// log <<< "depart Idle"
// log <<< "exit Idle"
// log <<< "action of transition is at Stationary"
// log <<< "enter Alert"
// log <<< "arrive Alert"

m.state('-> Sprinting');
// log <<< "depart Alert"
// log <<< "exit Alert"
// log <<< "exit Stationary"
// log <<< "action of transition is at the root state"
// log <<< "enter Moving"
// log <<< "enter Running"
// log <<< "enter Sprinting"
// log <<< "arrive Sprinting"
class Mover
  state @::
  
    # Programmatically set up the root to log the transitional
    # events of all states
    construct: ->
      events = ['depart', 'exit', 'enter', 'arrive']
      for s in [this].concat @substates true
        for e in events
          do ( s, e ) -> s.on e, -> console.log "#{e} #{@name()}"

    Stationary:
      Idle: state 'initial'
      Alert: state
    Moving:
      Walking: state
      Running:
        Sprinting: state

    transitions:
      Announcing: source: '*', target: '*'
        action: ->
          name = @superstate().name() or "the root"
          @end "action of transition is at {name}"
        end: ( message ) -> console.log message


m = new Mover

m.state '-> Alert'
# log <<< "depart Idle"
# log <<< "exit Idle"
# log <<< "action of transition is at Stationary"
# log <<< "enter Alert"
# log <<< "arrive Alert"

m.state '-> Sprinting'
# log <<< "depart Alert"
# log <<< "exit Alert"
# log <<< "exit Stationary"
# log <<< "action of transition is at the root"
# log <<< "enter Moving"
# log <<< "enter Running"
# log <<< "enter Sprinting"
# log <<< "arrive Sprinting"

See also: Transitional events

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.

Transition TransitionExpression StateController.privileged.change

⏎  Transitions  <  Concepts  <  Overview

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

construct destroy

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

State : depart exit enter arrive

Transition : enter start end exit

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 action function calls end, signifying the end of its action phase, the transition 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 entered 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: The transition lifecycle

Mutation events

mutate

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.

var flavors = [
    'vanilla',
    'chocolate',
    'strawberry',
    'AmeriCone Dream'
];

function Kid () {}
state( Kid.prototype, 'mutable', {
    data: {
        favorite: 'chocolate'
    },

    waver: function () {
        var i = Math.random() * flavors.length << 0;
        this.data({ favorite: flavors[i] });
    },
    whine: function ( complaint ) {
        if ( typeof console !== 'undefined' ) {
            console.log( complaint );
        }
    },

    mutate: function ( mutation, delta, before, after ) {
        this.owner().whine(
            "I hate " + delta.favorite +
            ", I want " + mutation.favorite + "!"
        );
    }
});


var jr = new Kid;

// We could have added listeners this way also:
jr.state().on( 'mutate', function ( mutation, delta ) { /*...*/ } );

jr.waver();  // log <<< "I hate chocolate, I want strawberry!"
jr.waver();  // log <<< "I hate strawberry, I want chocolate!"
jr.waver();  // No whining! On a whim, junior stood pat this time.
jr.waver();  // log <<< "I hate chocolate, I want AmeriCone Dream!"
flavors = [
  'vanilla'
  'chocolate'
  'strawberry'
  'AmeriCone Dream'
]

class Kid
  state @::, 'mutable'
    data:
      favorite: 'chocolate'

    waver: ->
      @data favorite: flavors[ Math.random() * flavors.length << 0 ]
    whine: ( complaint ) ->
      console?.log complaint

    mutate: ( mutation, delta, before, after ) ->
      @owner.whine "I hate #{ delta.favorite }, " +
                   "I want #{ mutation.favorite }!"


jr = new Kid

# We could have added listeners this way also:
jr.state().on 'mutate', ( mutation, delta ) -> # ...

jr.waver()  # log <<< "I hate chocolate, I want strawberry!"
jr.waver()  # log <<< "I hate strawberry, I want chocolate!"
jr.waver()  # No whining! On a whim, junior stood pat this time.
jr.waver()  # log <<< "I hate chocolate, I want AmeriCone Dream!"

state/mutation.js

Custom event types

A state’s emit method allows any type of event to be broadcast and consumed.

function Kid () {}
state( Kid.prototype, {
    Happy: state,
    Sad: state,

    events: {
        gotIceCream: function () { this.be('Happy'); },
        spilledIceCream: function () { this.be('Sad'); }
    }
});


var jr = new Kid;

jr.state().emit('gotIceCream');
jr.state();                          // >>> State 'Happy'

jr.state().emit('spilledIceCream');
jr.state();                          // >>> State 'Sad'
class Kid
  state @::
    Happy: state
    Sad: state

    events:
      gotIceCream: -> @be 'Happy'
      spilledIceCream: -> @be 'Sad'


jr = new Kid

jr.state().emit 'gotIceCream'
jr.state()                         # >>> State 'Happy'

jr.state().emit 'spilledIceCream'
jr.state()                         # >>> State 'Sad'

State.privileged.emit

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.

function DivisibleByThreeComputer () {
    state( this, 'abstract', {
        s0: state( 'default initial',
            { '0':'s0', '1':'s1' } ),
        s1: { '0':'s2', '1':'s0' },
        s2: { '0':'s1', '1':'s2' }
    });
}
DivisibleByThreeComputer.prototype.compute = function ( number ) {
    var i, l, binary = number.toString(2);
    this.state('->'); // reset
    for ( i = 0, l = binary.length; i < l; i++ ) {
        this.state().emit( binary[i] );
    }
    return this.state().is('s0');
}

var three = new DivisibleByThreeComputer;

three.compute( 8 );          // >>> false
three.compute( 78 );         // >>> true
three.compute( 1000 );       // >>> false
three.compute( 504030201 );  // >>> true
class DivisibleByThreeComputer
  constructor: ->
    state this, 'abstract'
      s0: state 'default initial'
          '0':'s0', '1':'s1'
      s1: '0':'s2', '1':'s0'
      s2: '0':'s1', '1':'s2'

  compute: ( number ) ->
    @state '->' # reset
    @state().emit symbol for symbol in number.toString 2
    @state().is 's0'

three = new DivisibleByThreeComputer

three.compute 8          # >>> false
three.compute 78         # >>> true
three.compute 1000       # >>> false
three.compute 504030201  # >>> true
⏎  Events  <  Concepts  <  Overview

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.

var object = {};
state( object, {
    A: state( 'initial', {
        admit: false,
        release: { D: false }
    }),
    B: state( 'mutable', {
        data: { bleep: 'bleep' },
        release: {
            'C, D': true,
            'C.**': false
        }
    }),
    C: {
        data: { blorp: 'blorp' },
        admit: true,
        C1: {
            C1a: state
        },
        C2: state
    },
    D: {
        enter: function () {
            this.$('B').removeGuard( 'admit' );
        },
        admit: function ( fromState ) {
            return 'blorp' in fromState.data()
        },
        release: function ( toState ) {
            return 'bleep' in toState.data()
        }
    }
});
object = {}
state object,
  A: state 'initial'
    admit: false
    release: D: false
  B: state 'mutable'
    admit: false
    release:
      'C, D': true
      'C.**': false
    data: bleep: 'bleep'
  C:
    data: blorp: 'blorp'
    C1:
      C1a: state
    C2: state
  D:
    enter: -> @$('B').removeGuard 'admit'
    admit: ( fromState ) -> 'blorp' of fromState.data()
    release: ( toState ) -> 'bleep' of toState.data()

Here we observe state guards imposing the following restrictions:

  • object initializes into state A, but upon leaving it may never return; we’ve also specifically disallowed direct transitions from A to D.

  • State B disallows entry from anywhere (for now), and releases conditionally to C or D but not directly to any descendant states of C; we also note its data item bleep.

  • State C imposes no guards, but we note its data item blorp.

  • State D “unlocks” B; it is also guarded by checking the opposing state’s data, allowing admission only from states with a data item keyed blorp, and releasing only to states with data item bleep.

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.

StateController evaluateGuard StateController::getTransitionExpressionFor

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.

function Scholar () {}
state( Scholar.prototype, 'abstract', {
    Matriculated: state( 'initial', {
        graduate: function ( gpa ) {
            this.owner().gpa = gpa;
            this.change( 'Graduated' );
        }
    }),
    Graduated: state( 'final' ),

    transitions: {
        Summa: {
            origin: 'Matriculated', target: 'Graduated',
            admit: function () { return this.data().gpa >= 3.9; },
            action: function () { /* swat down offers */ }
        },
        Magna: {
            origin: 'Matriculated', target: 'Graduated',
            admit: function () {
                var gpa = this.data().gpa;
                return 3.75 <= gpa && gpa < 3.9;
            },
            action: function () { /* choose internship */ }
        },
        Laude: {
            origin: 'Matriculated', target: 'Graduated',
            admit: function () {
                var gpa = this.data().gpa;
                return 3.50 <= gpa && gpa < 3.75;
            },
            action: function () { /* brag to the cat */ }
        },
        '': {
            origin: 'Matriculated', target: 'Graduated',
            action: function () { /* blame rounding error */ }
        }
    }
});

var scholar = new Scholar;
scholar.graduate( 3.4999 );
class Scholar
  state @::, 'abstract'
    Matriculated: state 'initial'
      graduate: ( gpa ) ->
        @owner().gpa = gpa
        @$ -> 'Graduated'

    Graduated: state 'final'
  
    transitions:
      Summa: transition
        origin: 'Matriculated', target: 'Graduated'
        admit: -> @owner().gpa >= 3.9
        action: -> # swat down offers
  
      Magna: transition
        origin: 'Matriculated', target: 'Graduated'
        admit: -> 3.75 <= @owner().gpa < 3.9
        action: -> # choose internship
  
      Laude: transition
        origin: 'Matriculated', target: 'Graduated'
        admit: -> 3.50 <= @owner().gpa < 3.75
        action: -> # brag to the cat
  
      '': transition
        origin: 'Matriculated', target: 'Graduated'
        action: -> # blame rounding error

scholar = new Scholar
scholar.graduate 3.4999

StateController evaluateGuard StateController::getTransitionExpressionFor

⏎  Guards  <  Concepts  <  Overview

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

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.

History

Any state may be ordered 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 change to 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

  • Add closed and open attributes or similar to allow contents of State instances to be either hidden or exposed. Closed retains some measure of enforcement of immutability, while open would reduce memory overhead.

  • 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 Harmony Proxies on supporting platforms to more deeply embed the state implementation into objects.