steelbreeze/state

View on GitHub
src/Transition.ts

Summary

Maintainability
A
0 mins
Test Coverage
import { Constructor, Predicate } from '@steelbreeze/types';
import { log, PseudoStateKind, TransitionKind, Region, PseudoState, State } from '.';
import { Vertex } from './Vertex';
import { Transaction } from './Transaction';
import { Behaviour } from './types';

/**
 * Type of the transition strategy
 * @hidden
 */
type TransitionStrategy<TTrigger> = (transaction: Transaction, deepHistory: boolean, trigger: any, transition: Transition<TTrigger>) => void;

/**
 * A transition changes the active state configuration of a state machine by specifying the valid transitions between states and the trigger events that cause them to be traversed.
 * @param TTrigger The type of trigger event that this transition will respond to.
 */
export class Transition<TTrigger = any> {
    /**
     * The target vertex of the transition.
     */
    public target: Vertex;

    /**
     * The optional guard condition that can restrict the transision being traversed based on trigger event type.
     * @internal
     * @hidden
     */
    private typeGuard: Predicate<TTrigger> = () => true;

    /**
     * The optional guard condition that can  restrict the transition being traversed based on user logic.
     * @internal
     * @hidden
     */
    private userGuard: Predicate<TTrigger> = () => true;

    /**
     * The user defined actions that will be called on transition traversal.
     * @internal
     * @hidden
     */
    actions: Array<Behaviour<TTrigger>> = [];

    /**
     * The precise semantics of the transition traversal based on the transition type.
     * @internal
     * @hidden
     */
    private execute: TransitionStrategy<TTrigger>;

    /**
     * Creates a new instance of the Transition class. By defaily, this is an internal transition.
     * @param source The source vertex of the transition.
     * @internal
     * @hidden
     */
    constructor(public readonly source: Vertex) {
        this.target = source;
        this.execute = internalTransition;

        this.source.outgoing.push(this);
    }

    /**
     * Adds an event type constraint to the transition; it will only be traversed if a trigger event of this type is evaluated.
     * @param eventType The type of trigger event that will cause this transition to be traversed.
     * @return Returns the transitions thereby allowing a fluent style transition construction.
     */
    on(eventType: Constructor<TTrigger>): this {
        this.typeGuard = (trigger: any) => trigger.constructor === eventType;

        return this;
    }

    /**
     * Adds an guard condition to the transition; it will only be traversed if the guard condition evaluates true for a given trigger event.
     * @param guard A boolean predicate callback that takes the trigger event as a parameter.
     * @return Returns the transitions thereby allowing a fluent style transition construction.
     * @remarks It is recommended that this is used in conjunction with the on method, which will first test the type of the trigger event.
     */
    when(guard: Predicate<TTrigger>): this {
        this.userGuard = guard;

        return this;
    }

    /**
     * Specifies a target vertex of the transition and the semantics of the transition.
     * @param target The target vertex of the transition.
     * @param kind The kind of transition, defining the precise semantics of how the transition will impact the active state configuration.
     * @return Returns the transitions thereby allowing a fluent style transition construction.
     */
    to(target: Vertex, kind: TransitionKind = TransitionKind.External): this {
        this.target = target;

        if (kind === TransitionKind.External) {
            this.execute = externalTransition<TTrigger>(this.source, this.target);
        } else {
            this.execute = localTransition;
        }

        return this;
    }

    /**
     * Adds user-defined behaviour to the transition that will be called after the source vertex has been exited and before the target vertex is entered.
     * @param actions The action, or actions to call with the trigger event as a parameter.
     * @return Returns the transitions thereby allowing a fluent style transition construction.
     */
    effect(...actions: Array<Behaviour<TTrigger>>): this {
        this.actions.push(...actions);

        return this;
    }

    /**
     * Tests the trigger event against both the event type constraint and guard condition if specified.
     * @param trigger The trigger event.
     * @returns Returns true if the trigger event was of the event type and the guard condition passed if specified.
     * @internal
     * @hidden
     */
    evaluate(trigger: any): boolean {
        return this.typeGuard(trigger) && this.userGuard(trigger);
    }

    /**
     * Traverses a transition.
     * @param transaction The current transaction being executed.
     * @param deepHistory True if deep history semantics are in play.
     * @param trigger The trigger event.
     * @internal
     * @hidden
     */
    traverse(transaction: Transaction, deepHistory: boolean, trigger: any): void {
        var transition: Transition | undefined = this;

        // initilise the composite with the initial transition
        const transitions: Array<Transition> = [transition];

        // For junction pseudo states, we must follow the transitions prior to traversing them
        while (transition.target instanceof PseudoState && (transition.target.kind & PseudoStateKind.Junction) && (transition = transition.target.getTransition(trigger))) {
            transitions.push(transition);
        }

        // execute the transition strategy for each transition in composite
        transitions.forEach(t => t.execute(transaction, deepHistory, trigger, t));
    }
}

/**
 * Internal transitions just execute the transition actions and then test for completion.
 * @hidden
 */
function internalTransition<TTrigger>(transaction: Transaction, deepHistory: boolean, trigger: any, transition: Transition<TTrigger>): void {
    log.write(() => `${transaction.instance} traverse internal transition at ${transition.target}`, log.Transition);

    transition.actions.forEach(action => action(trigger, transaction.instance));

    if (transition.target instanceof State) {
        transition.target.completion(transaction, deepHistory);
    }
}

/**
 * Local transitions find must find the most common inactive vertex in the target ancestry, then exit its active sibling and enter the inactive vertex.
 * @hidden
 */
function localTransition<TTrigger>(transaction: Transaction, deepHistory: boolean, trigger: any, transition: Transition<TTrigger>): void {
    log.write(() => `${transaction.instance} traverse local transition to ${transition.target}`, log.Transition);

    // Find the first inactive vertex abode the target
    const vertexToEnter = toEnter(transaction, transition.target);

    // exit the active sibling of the vertex to enter
    if (!vertexToEnter.isActive(transaction) && vertexToEnter.parent) {
        const vertex = transaction.getVertex(vertexToEnter.parent);

        if (vertex) {
            vertex.doExit(transaction, deepHistory, trigger);
        }
    }

    transition.actions.forEach(action => action(trigger, transaction.instance));

    if (vertexToEnter && !vertexToEnter.isActive(transaction)) {
        vertexToEnter.doEnter(transaction, deepHistory, trigger, undefined);
    }
}

/**
 * External transitions find the least common ancester between source and target, then exit the element below it on the source side and enter the sibling on the target side.
 * @hidden
 */
function externalTransition<TTrigger>(source: Vertex, target: Vertex): TransitionStrategy<TTrigger> {
    // create iterators over the source and target vertex ancestry
    const sourceIterator = ancestry(source);
    const targetIterator = ancestry(target);

    // get the first result from each iterator (this will always be the state machine root element)
    let sourceResult = sourceIterator.next();
    let targetResult = targetIterator.next();

    // the set of elements that need to entered within the target ancestry
    let toEnter: Array<Region | Vertex> = [];

    // iterate through all the common ancestors
    do {
        // set the actual source/target elements
        source = sourceResult.value;
        toEnter = [targetResult.value];

        sourceResult = sourceIterator.next();
        targetResult = targetIterator.next();
    } while (source === toEnter[0] && !sourceResult.done && !targetResult.done);

    // all elements past the common ancestor on the target side must be entered
    while (!targetResult.done) {
        toEnter.push(targetResult.value);

        targetResult = targetIterator.next();
    }

    // if the target is a history pseudo state, remove it (as normal history behaviour its the parent region is required)
    if (target instanceof PseudoState && target.kind & PseudoStateKind.History) {
        toEnter.pop();
    }

    // genrate the function to call when traversing the transition
    return (transaction: Transaction, deepHistory: boolean, trigger: any, transition: Transition<TTrigger>): void => {
        log.write(() => `${transaction.instance} traverse external transition from ${source} to ${target}`, log.Transition);

        // exit the source vertex
        source.doExit(transaction, deepHistory, trigger);

        // call the transition actions
        transition.actions.forEach(action => action(trigger, transaction.instance));

        // enter all elements from below the common ancestor to the target
        toEnter.forEach((t, i) => t.doEnter(transaction, deepHistory, trigger, toEnter[i + 1]));
    }
}

/**
* Returns the ancestry of this element from the root element of the hierarchy to this element.
* @returns Returns an iterable iterator used to process the ancestors.
* @internal
* @hidden
*/
function* ancestry(element: Region | Vertex): IterableIterator<Region | Vertex> {
    if (element.parent) {
        yield* ancestry(element.parent);
    }

    yield element;
}

/**
 * Determines the vertex that will need to be entered; the first non-active vertex in the ancestry above the target vertex.
 * @param transaction 
 * @param vertex 
 * @returns 
 * @hidden
 */
function toEnter(transaction: Transaction, vertex: Vertex): Vertex {
    while (vertex.parent && vertex.parent.parent && !vertex.parent.parent.isActive(transaction)) {
        vertex = vertex.parent.parent;
    }

    return vertex;
}