stateless

repository·master·Indexed 23 days ago

https://github.com/qmuntal/stateless

A Go library for creating state machines and lightweight state machine-based workflows, based on UML statechart theory. It is an idiomatic Go port of the C# 'stateless' library. It supports hierarchical states, guard clauses for conditional transitions, parameterized triggers, and external state storage for persistence. The library provides a fluent API for configuring transitions via StateConfiguration and supports both immediate and queued firing modes.

Tokens
5.5K
Snippets
8
Records
29
Agent score
80%

What's inside stateless

  1. How hierarchical states work

    master

    Stateless supports hierarchical states (substates). A substate is a state that exists within a superstate. When a state machine is in a substate, it is also considered to be in the superstate.

    Key behaviors:

    • Use .SubstateOf(superState) to define a hierarchy.
    • StateMachine.IsInState(state) returns true if the machine is in the specified state OR any of its substates.
    • StateMachine.State returns the precise current substate.
    • Entry/Exit events for the superstate are only triggered when entering or leaving the superstate itself, not when moving between its substates.
    phoneCall.Configure(stateOnHold).
      SubstateOf(stateConnected).
      Permit(triggerTakenOffHold, stateConnected).
      Permit(triggerPhoneHurledAgainstWall, statePhoneDestroyed)
  2. Create a basic state machine with stateless.NewStateMachine

    master

    To create a state machine, use stateless.NewStateMachine(initialState). You then configure states using .Configure(state) and define transitions using .Permit(trigger, destinationState). You can also attach entry and exit handlers to states or specific transitions.

    Common methods for configuration:

    • .Configure(state): Starts configuration for a specific state.
    • .Permit(trigger, destinationState): Defines a transition from the current state to a new state when a trigger is fired.
    • .OnEntry(func): Defines a callback executed when entering a state.
    • .OnExit(func): Defines a callback executed when exiting a state.
    • .OnEntryFrom(trigger, func): Defines a callback executed when entering a state specifically via a certain trigger.
    • .Fire(trigger, args...): Executes a trigger, potentially passing arguments to handlers.
    phoneCall := stateless.NewStateMachine(stateOffHook)
    
    phoneCall.Configure(stateOffHook).Permit(triggerCallDialed, stateRinging)
    
    phoneCall.Configure(stateRinging).
      OnEntryFrom(triggerCallDialed, func(_ context.Context, args ...any) error {
        onDialed(args[0].(string))
        return nil
      }).
      Permit(triggerCallConnected, stateConnected)
    
    phoneCall.Configure(stateConnected).
      OnEntry(func(_ context.Context, _ ...any) error {
        startCallTimer()
        return nil
      }).
      OnExit(func(_ context.Context, _ ...any) error {
        stopCallTimer()
        return nil
      }).
      Permit(triggerLeftMessage, stateOffHook).
      Permit(triggerPlacedOnHold, stateOnHold)
    
    // ...
    
    phoneCall.Fire(triggerCallDialed, "qmuntal")
  3. Use external state storage with stateless

    master

    If you need to persist the state machine's state (e.g., in a database via an ORM), use the specialized constructors. This allows the StateMachine to be stateless itself by reading/writing to your own storage.

    Option 1: Store only the State Use NewStateMachineWithExternalStorage by providing a getter and a setter function.

    Option 2: Store State and Arguments Use NewStateMachineWithExternalStorageAndArgs if you need to persist the arguments passed to the last trigger (useful for error states or metadata).

    Note: stateless.FiringQueued is an example of a firing strategy that can be passed to these constructors.

    // Store only state
    machine := stateless.NewStateMachineWithExternalStorage(func(_ context.Context) (stateless.State, error) {
      return myState.Value, nil
    }, func(_ context.Context, state stateless.State) error {
      myState.Value  = state
      return nil
    }, stateless.FiringQueued)
    
    // Store state and arguments
    machine := stateless.NewStateMachineWithExternalStorageAndArgs(func(_ context.Context) (stateless.State, []any, error) {
      return myState.Value, myState.Args, nil
    }, func(_ context.Context, state stateless.State, args ...any) error {
      myState.Value = state
      myState.Args = args
      return nil
    }, stateless.FiringQueued)
  4. Understand firing modes in Stateless

    master

    Stateless supports different execution strategies for handling triggers, known as firing modes. These modes determine whether a trigger is processed immediately or queued for sequential execution.

    1. Immediate Mode (fireModeImmediate): Triggers are processed synchronously as soon as they are fired. This mode is used when you want the state machine to transition immediately in response to a trigger.
    2. Queued Mode (fireModeQueued): Triggers are enqueued and processed sequentially. This prevents reentrancy issues by ensuring that one trigger's execution completes before the next one begins, even if multiple triggers are fired concurrently.
  5. FiringMode: Queued vs Immediate

    master

    The FiringMode determines how triggers are processed:

    • FiringQueued (Recommended): Ensures run-to-completion semantics. Triggers are queued and processed one by one, preventing race conditions during state transitions.
    • FiringImmediate: Triggers are processed immediately. This does not guarantee run-to-completion, so care must be taken to protect callbacks against race conditions.
  6. Manage state lifecycle with stateRepresentation

    master

    The stateRepresentation type is the core internal structure used to manage a state's lifecycle, including its hierarchy, entry/exit actions, and trigger behaviors. While primarily used by the library's engine, understanding its methods is key to understanding how states behave.

    Key lifecycle capabilities:

    • Hierarchical Support: States can have Superstates and Substates, allowing for nested state machines.
    • Entry/Exit Actions: Execute logic when entering or exiting a state via Enter and Exit methods.
    • Activation/Deactivation: Execute logic when a state (or its hierarchy) is activated or deactivated.
    • Trigger Handling: Uses AddTriggerBehaviour to register how a state responds to specific Triggers.
    • Guard Clauses: Triggers can be restricted by guard conditions that must be met for the behavior to execute.
  7. Define hierarchical state relationships with SubstateOf

    master

    The SubstateOf(superstate State) method allows you to create a hierarchy where states can be nested.

    Key behaviors of substates:

    1. Inheritance: Substates inherit the allowed transitions of their superstate.
    2. Lifecycle Propagation:
      • When entering a substate directly from outside the superstate, the entry actions for the superstate are executed first.
      • When leaving a substate to a state outside the superstate, the exit actions for the superstate are executed.
    3. Validation: The library prevents illegal cyclic configurations (e.g., a state being a substate of itself or creating a loop in the hierarchy) by panicking during configuration.

    Note: Panics occur during the configuration phase if a cycle is detected.

  8. Understand triggerBehaviour and its implementations

    master

    The triggerBehaviour interface defines how a trigger behaves during state machine execution, specifically regarding guard evaluation and the underlying Trigger value. While the interface is used internally to drive the state machine, its implementations represent different transition types:

    • transitioningTriggerBehaviour: A standard transition to a specific Destination state.
    • reentryTriggerBehaviour: A transition that re-enters the current state (or a specific Destination state).
    • dynamicTriggerBehaviour: A transition where the Destination state is determined dynamically by a function: func(context.Context, ...any) (State, error).
    • internalTriggerBehaviour: A trigger that executes an ActionFunc instead of (or in addition to) changing states.
    • ignoredTriggerBehaviour: A trigger that is recognized but results in no state change.
  9. Initialize a StateMachine

    master

    You can create a state machine using several constructors depending on how you want to manage state and handle trigger execution:

    1. NewStateMachine(initialState State): Creates a queued state machine (recommended) that manages its own state in-memory using a mutex.
    2. NewStateMachineWithMode(initialState State, firingMode FiringMode): Allows choosing between FiringQueued (run-to-completion) and FiringImmediate (no guaranteed run-to-completion).
    3. NewStateMachineWithExternalStorage(stateAccessor, stateMutator, firingMode): Uses provided functions to read and write state to an external source.
    4. NewStateMachineWithExternalStorageAndArgs(...): Similar to external storage, but also allows retaining arguments passed during state mutations.

    State and Trigger are type aliases for any, allowing you to use any comparable type (strings, ints, custom structs) to represent them.

  10. Configure state transitions and actions

    master

    To define how a state behaves, use the Configure(state State) method. This returns a *StateConfiguration object used to define:

    • Transitions: Mapping a Trigger to a destination State.
    • Entry/Exit Actions: Functions that run when entering or leaving a state.
    • Guards: Conditions that must be met for a transition to occur.

    Example flow:

    1. Call sm.Configure(MyState).
    2. Use the returned configuration to define transitions and actions.
    3. Use sm.Fire(trigger) to trigger transitions.
  11. Use parameterized triggers

    master

    You can assign strongly-typed parameters to triggers using SetTriggerParameters. This allows you to pass data through Fire that is then available in entry/exit handlers.

    Warning: If the parameters passed to Fire do not match the types specified in SetTriggerParameters, the application will panic.

    stateMachine.SetTriggerParameters(triggerCallDialed, reflect.TypeOf(""))
    
    stateMachine.Configure(stateRinging).
      OnEntryFrom(triggerCallDialed, func(_ context.Context, args ...any) error {
        fmt.Println(args[0].(string))
        return nil
      })
    
    stateMachine.Fire(triggerCallDialed, "qmuntal")
  12. Implement guard clauses for conditional transitions

    master

    Guard clauses allow you to define conditional transitions. When a trigger is fired, the state machine evaluates the guards to decide which transition to take.

    Rules for guards:

    • Guards must be mutually exclusive (only one can be valid at a time for a given trigger in a state).
    • Guards should be side-effect free.
    • Substates can override transitions by respecifying them, but they cannot disallow transitions allowed by a superstate.
    phoneCall.Configure(stateOffHook).
      Permit(triggerCallDialled, stateRinging, func(_ context.Context, _ ...any) bool {
        return IsValidNumber()
      }).
      Permit(triggerCallDialled, stateBeeping, func(_ context.Context, _ ...any) bool {
        return !IsValidNumber()
      })