Skip to content

Mastering State Machines in TaskTonic

TaskTonic Philosophy

When building asynchronous systems, keeping track of "what the system is currently doing" can quickly devolve into a nightmare of boolean flags (is_connecting, has_data, is_waiting).

TaskTonic eliminates this spaghetti code by building a highly efficient State Machine directly into the core ttTonic class [1-3].

Here is everything you need to know about why, when, and how to use state machines in your Sparkling Programming journey.


1. Why and When to Use a State Machine?

A State Machine restricts which actions are valid based on the current "mode" (or state) of the application.

When to use one: * Complex Asynchronous Flows: Like network communication (e.g., disconnected -> connecting -> authenticating -> ready) [4-6]. * User Interfaces: Managing wizard flows, disabled/enabled button states, or traffic light simulations [7, 8]. * Hardware Control: Ensuring a motor can only receive a "move" command if it is in an idle state, not if it is in an error or moving state.

When NOT to use one: State machines are not mandatory in TaskTonic [2, 9]. If your Tonic is simply a data pipeline (e.g., it receives an event, processes it, and spits it out), or a simple standalone worker, a state machine is overkill. You can just use generic "flat" sparkles (e.g., ttsc__process_data) without ever calling to_state(). If you never call to_state(), the Tonic remains in its default stateless mode (state = -1) [10, 11].


2. How it Works: The State Lifecycle

In TaskTonic, you don't need to define massive dictionary structures to configure states. You simply transition between them using self.to_state('state_name') [3, 11, 12].

The Transition Sequence

A critical concept to grasp is that calling self.to_state() does not immediately change the state and interrupt your current code [11]. Instead, it queues up a highly specific sequence of events that guarantees data integrity.

If you call self.to_state('new_state') inside a Sparkle, the exact sequence of execution is [11, 13]:

  1. Current Sparkle Finishes: The rest of the code in your current Sparkle executes to completion [11].
  2. on_exit is Triggered: TaskTonic automatically executes the exit handler for the old state [13].
  3. State Transition: The internal state variable is updated to the new state [13, 14].
  4. on_enter is Triggered: TaskTonic automatically executes the enter handler for the new state [13].

Because these transitions are scheduled as rapid extra_sparkles behind the scenes, the Catalyst executes the entire block seamlessly before grabbing the next user command from the queue [13, 15].


3. The Queueing Secret: Late State Binding

This is arguably the most powerful feature of TaskTonic’s architecture, and it is vital for concurrency:

TaskTonic checks the state when a Sparkle is executed, NOT when it is placed on the queue [16, 17].

Imagine a UI with two fast button clicks: 1. User clicks "Disconnect" (queues ttsc__disconnect). 2. A millisecond later, user clicks "Send Data" (queues ttsc__send_data).

If the "Disconnect" Sparkle changes the state from connected to offline, what happens to the pending "Send Data" Sparkle already sitting in the queue?

When the Catalyst pulls ttsc__send_data from the queue, it dynamically checks the Tonic's state at that exact moment [16]. Because the state is now offline, TaskTonic looks for ttsc_offline__send_data. If you haven't defined that, it safely falls back to a generic handler or silently does nothing (_noop) [3, 11, 18, 19].

You never have to write if self.is_connected: inside your Sparkles. The queue inherently protects you from stale asynchronous calls!


4. The Syntax for State Sparkles

TaskTonic uses smart naming conventions (introspection) to route your Sparkles. You don't need to register anything manually [20-23].

The general syntax pattern is: prefix_[state]__name [3, 21, 24].

A. The Lifecycle Hooks

These are automatically called during transitions [25, 26]. * ttse__on_enter(self): Called whenever any state is entered [25]. * ttse__on_exit(self): Called whenever any state is exited [25]. * ttse_<state>__on_enter(self): Called only when entering a specific state (e.g., ttse_waiting__on_enter) [25, 26].

B. State-Specific Sparkles

You can restrict a command or event so it behaves differently (or only exists) in a specific state [3, 19, 27].

class SmartDoor(ttTonic):
    def ttse__on_start(self):
        self.to_state('locked') # Initial state [28]

    # --- STATE: LOCKED ---
    def ttse_locked__on_enter(self):
        self.log("The door is now locked.") # [28]

    def ttsc_locked__open(self):
        self.log("Cannot open. Door is locked!")

    def ttsc_locked__unlock(self):
        self.to_state('unlocked')

    # --- STATE: UNLOCKED ---
    def ttsc_unlocked__open(self):
        self.log("Opening the door...")
        self.to_state('open')

    def ttsc_unlocked__lock(self):
        self.to_state('locked')

C. The Fallback Chain

What happens if another Tonic calls door.ttsc__open() while the door is in an unknown state? TaskTonic follows a strict resolution order [3, 19, 29]:

  1. Specific: It looks for ttsc_<current_state>__open.
  2. Generic (Fallback): If the specific method doesn't exist, it looks for a stateless fallback: ttsc__open [19, 30].
  3. No-Op: If neither exists, it safely ignores the command using an internal _noop (doing nothing) [11, 19].

This allows you to write incredibly clean code. Define the generic behavior once, and only override it with a state-specific ttsc_<state>__name when the behavior needs to deviate!