clj-statecharts

repository·master·Indexed 42 days ago

https://github.com/lucywang000/clj-statecharts

A Clojure(Script) library for implementing State Machines and StateCharts, inspired by XState. It supports declarative definitions, hierarchical and parallel states, guarded transitions, and delayed transitions. The library provides both a purely functional Immutable API for integration with systems like re-frame and a stateful Service API for standalone state management. Version 0.0.1.

Tokens
9K
Snippets
20
Records
38
Agent score
53%

What's inside clj-statecharts

  1. Overview of clj-statecharts features

    master

    clj-statecharts is a library for implementing State Machines and StateCharts in Clojure(Script), inspired by XState. It provides a declarative way to define complex state logic.

    Key supported features include:

    • Declarative statecharts definition: Define your machine logic as data.
    • Compound (hierarchical) states: Support for nested state structures.
    • Transition actions & Entry/Exit actions: Execute logic during state changes or when entering/exiting specific states.
    • Guarded transitions: Conditional transitions based on specific criteria.
    • Delayed transitions: Support for transitions that occur after a specified delay.
    • re-frame integration: Built-in support for using statecharts within the re-frame ecosystem.
  2. Understand state representation for parallel nodes

    master

    The way the current state is represented depends on whether the parallel node is the root or a nested child:

    1. Root Parallel Node: The entire state is represented as a map of its regions.

      • Example: {:r1 :r1-state :r2 :r2-state}
    2. Nested Parallel Node: The state is represented as a hierarchical path where the parallel node's state is a map nested within the parent state.

      • Example: If :s1.1 is a parallel node with regions :r1 and :r2, the state is [:s1 {:s1.1 {:r1 :r1-state :r2 :r2-state}}].
  3. Choose between Immutable API and Service API for re-frame

    master

    When integrating clj-statecharts with re-frame, choose the integration method based on what drives your state changes:

    Integration MethodUse Case
    Immutable APIWhen state changes are primarily driven by UI/re-frame events (e.g., button clicks) or when you need to modify the state directly within other re-frame events.
    Service APIWhen state changes are primarily driven by non-UI/re-frame events (e.g., managing WebSocket states or background processes).
  4. Differentiate between internal and external transitions

    master

    When transitioning to a child state, you can choose whether or not to trigger the entry/exit actions of the parent state.

    • External Transition: Triggers the entry/exit actions of the parent state. Use the :> prefix in the target syntax.
    • Internal Transition: Does NOT trigger the entry/exit actions of the parent state. Use the :. prefix in the target syntax.

    This allows you to move between sub-states without re-running the logic associated with the container state.

    {:states {:s1 {:initial :s1.1
                   :entry entry1
                   :exit exit1
                   :on {:event1_1.2_internal [:. :s1.2]      ;; Internal: no entry/exit for :s1
                        :event1_1.2_external [:> :s1 :s1.2]} ;; External: triggers entry/exit for :s1
                   :states {:s1.1 {}
                            :s1.2 {}}}}}
  5. Compare clj-statecharts with XState

    master

    If you are coming from an XState background, note the following key differences and limitations in clj-statecharts:

    Key Differences

    • Data Types: clj-statecharts supports native ClojureScript objects (maps/vectors) as context. XState is limited to plain JS maps because it uses Object.assign for updates, which strips CLJS prototypes.
    • State Representation: clj-statecharts uses Clojure keywords (e.g., :waiting) for states, whereas XState uses strings.
    • State Map Shape: clj-statecharts uses a flat map (see State Map Structure) instead of XState's nested {value, context} structure.

    Unsupported Features

    The following XState/SCXML features are currently not supported in clj-statecharts:

    • Invoking another machine, promise, or actor.
    • Final states and done signals.
    • History states.
  6. How state storage works in clj-statecharts

    master

    clj-statecharts is designed to be mostly stateless. To manage stateful behavior, you must store the state objects in an external location, such as a [service], a [re-frame db], or a custom storage mechanism.

    As events trigger transitions, the state objects in your storage must be updated. While manual transitions (calling transition from your code) make it easy to update storage, [delayed transitions] require the library to update the storage automatically. To bridge this gap, clj-statecharts provides the IStore interface, which allows external transitioners to coordinate with internal delayed transitions to ensure the storage location remains synchronized.

  7. Understand the role of a Service

    master

    While Machines and States are immutable, a Service is the stateful container used to run them. A service is necessary for two primary reasons:

    1. State Management: It acts as a container for the state (typically implemented as an atom within the state record) that transitions over time as events are processed.
    2. Delayed Transitions: The service manages timers and scheduling. When a delayed transition is triggered, the service schedules a timer upon entering the state. If the machine transitions to a different state before the timer fires, the service is responsible for canceling that timer to prevent unintended transitions.
  8. Access the previous state using `_prev-state`

    master

    During the execution of an action, the state argument represents the new state. To access the state as it existed before the transition occurred, you can use the _prev-state key within the state's context.

    Important constraints:

    • _prev-state only exists during the transition execution; it is not available once the transition is complete.
    • If the transition is called with {:exec false}, actions are returned to the caller instead of being executed, and _prev-state will not be available.
  9. Understand the difference between Machine and State

    master

    In clj-statecharts, the system is modeled using two distinct immutable concepts:

    • Machine: An immutable specification of all possible transitions and states. Think of it as a map that contains directions and locations but is not currently 'at' any location.
    • State: An immutable representation of the system at a specific moment. Think of it as a place on the map. It represents where the system currently is and what data it currently holds.

    To use the library correctly, remember that the Machine defines the logic, while the State represents the current snapshot of that logic in action.

  10. Understand Hierarchical (Compound) States

    master

    Hierarchical states (also known as compound or nested states) allow you to organize a state machine as a tree structure rather than a flat list of states. This prevents 'state explosion' by allowing states to share logic through inheritance-like behavior.

    Key behaviors:

    • Nesting: A state can define its own sub-states (child states).
    • Event Bubbling: If a child state does not have a handler for a specific event, the event is passed up to its parent state. The search continues from the current node up to the root until a node with a matching handler is found.

    This allows you to define global events (like a clear-screen command) at the root level, ensuring they work across all sub-states without needing to redefine them in every single state.

    {:id :calculator
     :on {:clear-screen {:target  :operand1
                         :actions (assign reset-inputs)}}
     {:states
      {:operand1 {:on {:input-digit    :operand1
                       :input-operator :operator}}
    
       :operator {:on {:input-operator :operand2}}
    
       :operand2 {:on {:input-digit :operand2
                       :equals      :result}}}}}
  11. Understand the state map structure in clj-statecharts

    master

    Unlike XState, which uses a nested structure with value and context keys, clj-statecharts uses a flat map for the state.

    In this flat map:

    1. Internal Keys: Any key prefixed with an underscore (e.g., :_state) is reserved for internal library use. Your application code (such as context functions) should not modify these keys.
    2. Application Data: All other keys represent your application-specific data, serving the same purpose as context in XState. This flat structure is designed to make integration easier with existing Clojure/CLJS patterns, such as storing state within a re-frame DB.
    ;; clj-statecharts flat state map example
    {:_state :waiting
     :user   "jack"
     :backoff 3000}
  12. Understand the two layers of APIs

    master

    The library provides two distinct ways to interact with state machines:

    1. The Immutable API: A purely functional layer that deals directly with machines and states. It is ideal for integrating state machine logic into existing state management systems like re-frame.
    2. The Service API: A higher-level, stateful layer built on top of the Immutable API. It manages the machine's state for you, making it easier to get started for standalone use cases.