Dex – 1: State Machines
Description: Reference for Dex-1: State Machines

#### State Machines

`DexStateMachine` provides a small table-driven state machine for asynchronous workflows.

Use it when an object has a finite set of modes and transitions between those modes may need to wait for asynchronous work. Common examples include connection lifecycles, authentication handoff, device setup, or UI workflows where a request must move through well-defined states.

The state machine serializes transition requests internally, runs transition callbacks from a fiber, and rejects attempts to use edges that were not declared up front.

## Defining States

States are represented by a `GEnum` type. This lets libdex validate transition tables and return enum values from `dex_state_machine_transition()`.

    typedef enum
    {
      PASSWORD_DAEMON_INITIAL,
      PASSWORD_DAEMON_PREPARE,
      PASSWORD_DAEMON_READY,
      PASSWORD_DAEMON_LOCKED,
      PASSWORD_DAEMON_UNLOCKED,
    } PasswordDaemonState;

The enum must have a registered `GType`, for example using `G_DEFINE_ENUM_TYPE()`.

## Defining Transitions

A transition table declares every supported edge.

    static const DexStateTransition password_daemon_transitions[] = {
      { PASSWORD_DAEMON_INITIAL,  PASSWORD_DAEMON_PREPARE,  enter_prepare  },
      { PASSWORD_DAEMON_PREPARE,  PASSWORD_DAEMON_READY,    enter_ready    },
      { PASSWORD_DAEMON_READY,    PASSWORD_DAEMON_LOCKED,   enter_locked   },
      { PASSWORD_DAEMON_LOCKED,   PASSWORD_DAEMON_UNLOCKED, enter_unlocked },
      { PASSWORD_DAEMON_UNLOCKED, PASSWORD_DAEMON_LOCKED,   enter_locked   },
    };

The table is copied and validated when the `DexStateMachine` is created. Each entry must use valid enum values and have a transition callback. Duplicate edges are rejected.

Once created, the transition table is immutable. There is no API to add transitions while the state machine is running.

## Creating a State Machine

The constructor accepts user data that is passed to every transition callback. Use normal C ownership rules for this data. If the state machine should keep an object alive, pass a strong reference and destroy notify. If it should not, pass a weak-reference holder instead.

    daemon->state_machine =
      dex_state_machine_new (PASSWORD_DAEMON_TYPE_STATE,
                             PASSWORD_DAEMON_INITIAL,
                             password_daemon_transitions,
                             G_N_ELEMENTS (password_daemon_transitions),
                             NULL,
                             0,
                             g_object_ref (daemon),
                             g_object_unref);

## Writing Transition Callbacks

Transition callbacks use `DexStateTransitionFunc`.

    static gboolean
    enter_prepare (DexStateTransitionContext *context,
                   gpointer                   user_data,
                   GError                   **error)
    {
      PasswordDaemon *daemon = user_data;
      g_autoptr(GSocket) control = NULL;

      g_assert_cmpuint (dex_state_transition_context_get_from (context),
                        ==,
                        PASSWORD_DAEMON_INITIAL);
      g_assert_cmpuint (dex_state_transition_context_get_to (context),
                        ==,
                        PASSWORD_DAEMON_PREPARE);

      control = dex_await_object (create_control_socket (daemon), error);
      if (control == NULL)
        return FALSE;

      daemon->control = g_steal_pointer (&control);

      return TRUE;
    }

Callbacks are run from a fiber, so they may call `dex_await()` and `dex_await_*()` directly. They should return `TRUE` on success or `FALSE` with `error` set on failure.

If a callback succeeds without calling `dex_state_transition_context_set_state()`, the state machine commits the target state from `dex_state_transition_context_get_to()`. If the callback fails before setting state, the state remains unchanged.

Callbacks may also call `dex_state_transition_context_continue_to()` to continue through another declared edge before queued transition requests are processed.

## Requesting Transitions

Call `dex_state_machine_transition()` to request a transition. The returned future resolves to the final enum state or rejects with an error.

    DexFuture *future =
      dex_state_machine_transition (daemon->state_machine,
                                    PASSWORD_DAEMON_LOCKED);

Requests are serialized. If multiple callers request transitions at once, only one transition fiber runs at a time.

Use `dex_state_machine_get_requested_state()` to read the most recent valid target passed to `dex_state_machine_transition()`. This is useful when an owner wants to expose requested state separately from current state. It is updated when the request is made, before the transition runs. It does not mean that the requested transition will succeed, and it may differ from the current state after a rejected transition.

Unsupported transitions reject with `DEX_ERROR_INVALID_TRANSITION`. For example, a request to go directly from `PASSWORD_DAEMON_INITIAL` to `PASSWORD_DAEMON_UNLOCKED` will fail unless that edge exists in the transition table.

## Waiting for State

Call `dex_state_machine_wait_for_state()` when code outside the transition callback needs to wait until the current state reaches a specific value.

    DexFuture *future =
      dex_state_machine_wait_for_state (daemon->state_machine,
                                        PASSWORD_DAEMON_READY);

The returned future resolves immediately if the state machine is already in that state. Otherwise it resolves the next time the state is set to that value, including intermediate states exposed by `dex_state_transition_context_set_state()`.

Waiting does not request a transition. Pair it with `dex_state_machine_transition()` when the caller also needs to initiate the state change.

## Interrupting Active Transitions

Long-running transition callbacks can opt in to cooperative interruption with `dex_state_transition_context_wait_for_interrupt()`. The returned future resolves when another caller calls `dex_state_machine_interrupt()`.

    static gboolean
    enter_unlocked (DexStateTransitionContext  *context,
                    gpointer                    user_data,
                    GError                    **error)
    {
      PasswordDaemon *daemon = user_data;

      if (!dex_await (dex_future_first (
                        dex_state_transition_context_wait_for_interrupt (context),
                        password_daemon_read_loop (daemon),
                        NULL),
                      error))
        return FALSE;

      return dex_state_transition_context_continue_to (context,
                                                       PASSWORD_DAEMON_LOCKED,
                                                       error);
    }

Interrupting does not request a transition, change state, or skip the transition callback. It only resolves the active context’s interrupt future. The callback decides whether to continue to another state, return successfully, or fail with an error.

If `dex_state_machine_interrupt()` is called before the active callback requests the interrupt future, the interrupt is remembered and `dex_state_transition_context_wait_for_interrupt()` returns an already resolved future. If the callback completes before interruption, the interrupt future rejects with `G_IO_ERROR_CANCELLED`.

## Updating State During a Transition

`DexStateTransitionContext` gives callbacks access to both the declared transition edge and the real current state. The context is only valid for the duration of the transition callback and must not be stored.

`dex_state_transition_context_get_from()` and `dex_state_transition_context_get_to()` are fixed for the callback. They describe the transition table edge being executed. They do not change if the callback updates the state.

`dex_state_transition_context_get_state()` reads the real current state from the state machine. `dex_state_transition_context_set_state()` updates that real state immediately. This is useful for quiescent transitions that need to expose progress through intermediate states in one pass. `set_state()` may only be used while the transition callback is active.

    static gboolean
    enter_prepare (DexStateTransitionContext *context,
                   gpointer                   user_data,
                   GError                   **error)
    {
      PasswordDaemon *daemon = user_data;

      dex_state_transition_context_set_state (context, PASSWORD_DAEMON_PREPARE);

      if (!dex_await (password_daemon_prepare (daemon), error))
        return FALSE;

      dex_state_transition_context_set_state (context, PASSWORD_DAEMON_READY);

      return TRUE;
    }

With this table, requesting `INITIAL -> PREPARE` runs `enter_prepare()` and resolves with `READY` when the callback succeeds.

    static const DexStateTransition password_daemon_transitions[] = {
      { PASSWORD_DAEMON_INITIAL, PASSWORD_DAEMON_PREPARE, enter_prepare },
    };

If a callback calls `dex_state_transition_context_set_state()`, those updates are not rolled back automatically if the callback later fails. They are real state changes. Set an explicit failure state before returning `FALSE` when that is the state machine behavior you want.

## GObject State Properties

`DexStateMachine` is not a `GObject`. When a `GObject` owns a state machine, keep the state machine private and expose state from the owning object.

Install a readable enum property on the owner. Transition callbacks will emit `notify::state` explicitly when observable state changes.

    enum {
      PROP_0,
      PROP_STATE,
      N_PROPS
    };

    static GParamSpec *properties[N_PROPS];

    static PasswordDaemonState
    password_daemon_get_state (PasswordDaemon *daemon)
    {
      return dex_state_machine_get_state (daemon->state_machine);
    }

    static void
    password_daemon_get_property (GObject    *object,
                                  guint       prop_id,
                                  GValue     *value,
                                  GParamSpec *pspec)
    {
      PasswordDaemon *daemon = PASSWORD_DAEMON (object);

      switch (prop_id)
        {
        case PROP_STATE:
          g_value_set_enum (value, password_daemon_get_state (daemon));
          break;

        default:
          G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        }
    }

    static void
    password_daemon_class_init (PasswordDaemonClass *daemon_class)
    {
      GObjectClass *object_class = G_OBJECT_CLASS (daemon_class);

      object_class->get_property = password_daemon_get_property;

      properties[PROP_STATE] =
        g_param_spec_enum ("state", NULL, NULL,
                           PASSWORD_DAEMON_TYPE_STATE,
                           PASSWORD_DAEMON_INITIAL,
                           (G_PARAM_READABLE |
                            G_PARAM_STATIC_STRINGS));

      g_object_class_install_properties (object_class, N_PROPS, properties);
    }

If the owner stores a strong reference to the state machine, do not also pass a strong reference to the owner as transition user data. That creates a reference cycle:

    daemon -> state_machine -> daemon

Use a small weak-reference holder instead.

    typedef struct
    {
      GWeakRef daemon;
    } PasswordDaemonTransitionData;

    static PasswordDaemonTransitionData *
    password_daemon_transition_data_new (PasswordDaemon *daemon)
    {
      PasswordDaemonTransitionData *data;

      data = g_new0 (PasswordDaemonTransitionData, 1);
      g_weak_ref_init (&data->daemon, daemon);

      return data;
    }

    static void
    password_daemon_transition_data_free (PasswordDaemonTransitionData *data)
    {
      g_weak_ref_clear (&data->daemon);
      g_free (data);
    }

    daemon->state_machine =
      dex_state_machine_new (PASSWORD_DAEMON_TYPE_STATE,
                             PASSWORD_DAEMON_INITIAL,
                             password_daemon_transitions,
                             G_N_ELEMENTS (password_daemon_transitions),
                             NULL,
                             0,
                             password_daemon_transition_data_new (daemon),
                             (GDestroyNotify)password_daemon_transition_data_free);

When transition callbacks update the state machine state directly, notify the owner at the same time. A helper keeps this consistent.

    static void
    password_daemon_set_state (PasswordDaemon            *daemon,
                               DexStateTransitionContext *context,
                               PasswordDaemonState        state)
    {
      if (dex_state_transition_context_get_state (context) == state)
        return;

      dex_state_transition_context_set_state (context, state);
      g_object_notify_by_pspec (G_OBJECT (daemon), properties[PROP_STATE]);
    }

Then transition callbacks can acquire the owner from the weak reference and use the helper for every state change that should be visible through `:state`.

    static gboolean
    enter_prepare (DexStateTransitionContext  *context,
                   gpointer                    user_data,
                   GError                    **error)
    {
      PasswordDaemonTransitionData *data = user_data;
      g_autoptr(PasswordDaemon) daemon = NULL;

      daemon = g_weak_ref_get (&data->daemon);
      if (daemon == NULL)
        {
          g_set_error_literal (error,
                               G_IO_ERROR,
                               G_IO_ERROR_CANCELLED,
                               "PasswordDaemon was disposed");
          return FALSE;
        }

      password_daemon_set_state (daemon, context, PASSWORD_DAEMON_PREPARE);

      if (!dex_await (password_daemon_prepare (daemon), error))
        return FALSE;

      password_daemon_set_state (daemon, context, PASSWORD_DAEMON_READY);

      return TRUE;
    }

If `:state` notifications matter, do not rely on the state machine’s automatic target commit for those states. The automatic commit happens after the transition callback returns, where the owner has no callback-local opportunity to emit `notify::state`. Call `dex_state_transition_context_set_state()` through your notification helper instead.

The same rule applies when using `dex_state_transition_context_continue_to()`. If the current edge target should be observable, set and notify it before calling `continue_to()`. The continued edge callback should notify its own observable state changes.

Notifications are emitted from the transition fiber. If signal handlers for the owner must run on a specific main context or thread, create and use the state machine with a scheduler that runs there.

If callers need to observe both intent and progress, expose a separate `:requested-state` property on the owner and read it from `dex_state_machine_get_requested_state()`. Notify that property from the owner method that calls `dex_state_machine_transition()`, because `DexStateMachine` is not a `GObject`.

## Continuing Through Edges

`dex_state_transition_context_continue_to()` lets a transition callback run another declared edge immediately while the state machine still owns its serialization slot. It is synchronous composition: the next transition callback is called before `continue_to()` returns, and queued transition requests wait until the continuation chain finishes.

    static gboolean
    enter_prepare (DexStateTransitionContext  *context,
                   gpointer                    user_data,
                   GError                    **error)
    {
      PasswordDaemon *daemon = user_data;

      if (!dex_await (password_daemon_prepare (daemon), error))
        return FALSE;

      return dex_state_transition_context_continue_to (context,
                                                       PASSWORD_DAEMON_READY,
                                                       error);
    }

With this table, a request for `INITIAL -> PREPARE` runs `enter_prepare()`, commits `PREPARE`, then immediately runs `enter_ready()` before any queued transition request can run.

    static const DexStateTransition password_daemon_transitions[] = {
      { PASSWORD_DAEMON_INITIAL, PASSWORD_DAEMON_PREPARE, enter_prepare },
      { PASSWORD_DAEMON_PREPARE, PASSWORD_DAEMON_READY,   enter_ready   },
    };

If the callback has not called `dex_state_transition_context_set_state()`, `continue_to()` validates the next edge before committing the current edge. An unsupported continuation returns `FALSE` with `DEX_ERROR_INVALID_TRANSITION`. If the next callback fails after the current edge has been committed, that state change is not rolled back.

If a continued transition callback uses `dex_await()`, the same transition fiber suspends and later resumes; the main context is not blocked, but the state machine queue is still held. Chained continuations use ordinary C calls, so this API is intended for short, bounded handoffs between edges rather than unbounded graph traversal.

## Circular State Machines

Circular graphs are valid. For example, a lock state machine can declare both directions explicitly.

    static const DexStateTransition lock_transitions[] = {
      { PASSWORD_DAEMON_LOCKED,   PASSWORD_DAEMON_UNLOCKED, unlock },
      { PASSWORD_DAEMON_UNLOCKED, PASSWORD_DAEMON_LOCKED,   lock   },
    };

The state machine does not reject cycles. It rejects only invalid states, duplicate edges at construction time, and unsupported edges at transition time.

## Error Handling

There are three common failure cases:

- Construction fails if the transition table is invalid.
- Transition requests reject with `DEX_ERROR_INVALID_TRANSITION` when no declared edge exists.
- Transition callbacks may fail with their own error.

Callback failures leave the state unchanged only if the callback has not already called `dex_state_transition_context_set_state()`. Invalid explicit state updates are programmer errors.

## Related Pages

- Limiters
- Scheduler behavior and pinning