Preview of main at d0a501a, 2026-11-07

xstate is a header-only port of XState 5.33.2, Stately’s library of state machines and actors, to C++20, one of the libraries of webcpp. A machine is loaded from the JSON config XState’s createMachine takes, and run through XState’s pure functions or as an actor.

This page teaches xstate as Stately’s documentation teaches XState: each concept, why it exists, its smallest example, its variations and its rules. Every program it shows was compiled and run, natively and for wasm32-wasip2 and wasm32-wasip3, and printed the output shown under it. Every program is also proven against XState: its twin, the same program written for XState 5.33.2, prints the same output, or the output of a difference xstate makes on purpose, unless it shows something XState does not have (How the port is proven). The reference lists every name the headers declare.

Introduction

This chapter says what xstate is and why it exists, how its two layers share the work, how the port is proven against XState, and what a program needs to use it. A quick start then builds a first program, and the last section says how the rest of the page is laid out.

Overview

XState is Stately’s library of state machines, statecharts and actors for JavaScript and TypeScript. It models logic as event-driven state machines, and runs them as actors that send each other events. xstate ports XState 5.33.2 to C++20, header-only, in the namespace webcpp::xstate. Its machines are the ones a person writes for XState: a config is JSON with the shape of XState’s, loaded when the program runs rather than compiled into it. xstate runs a machine as XState does, transition for transition: through XState’s pure functions, one microstep at a time when its caller bounds the work of an event, or as an actor that invokes and spawns children, sends other actors events, hears its children finish, waits on timers and asks the host program for work done outside it.

xstate’s actor layer is built on webcpp’s xactor, a deterministic actor system whose actors exchange messages one at a time each, on one thread, in an order that depends only on the order the messages were sent, each execution with a budget of fuel, and with no time but the host’s.

In this documentation, XState is always Stately’s library, and xstate the port of its machines and actors.

A first look

Stately’s XState page introduces the library with a counter: a machine whose context holds a count, three events that change it, an actor that runs the machine, and a subscription that prints the count after each event. In xstate the counter is this program:

#include <webcpp/xstate/actors.hpp>

#include <boost/json.hpp>

#include <cstdint>
#include <iostream>
#include <utility>

namespace xstate = webcpp::xstate;

namespace {

/** An assign that adds `step` to the context's count. */
xstate::assign_action adding(std::int64_t step) {
    auto add = [step](const xstate::action_args& args) -> xstate::result<boost::json::object> {
        const boost::system::result<const boost::json::value&> count = args.context.try_at("count");
        if (!count.has_value()) {
            return count.error();
        }
        const boost::system::result<std::int64_t> number = count->try_as_int64();
        if (!number.has_value()) {
            return number.error();
        }
        return boost::json::object{{"count", *number + step}};
    };
    return xstate::assign_action{.assignment = std::move(add)};
}

/** An assign that sets the count to the event's `value`. */
xstate::result<boost::json::object> set_count(const xstate::action_args& args) {
    const boost::json::value* value = args.event.payload.if_contains("value");
    if (value == nullptr) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"count", *value}};
}

}  // namespace

int main() {
    const boost::json::value config = boost::json::parse(R"({
        "context": {"count": 0},
        "on": {
            "INC": {"actions": "increment"},
            "DEC": {"actions": "decrement"},
            "SET": {"actions": "set"}
        }
    })");
    xstate::implementations implementations;
    implementations.actions.emplace("increment", adding(1));
    implementations.actions.emplace("decrement", adding(-1));
    implementations.actions.emplace("set", xstate::assign_action{.assignment = set_count});
    const xstate::result<xstate::machine> count_machine =
        xstate::create_machine(config, std::move(implementations));
    if (!count_machine.has_value()) {
        return 1;
    }

    xstate::actor_system system({.fuel = 100});
    const xstate::result<xstate::actor_ref> count_actor = system.create_actor(*count_machine);
    if (!count_actor.has_value()) {
        return 1;
    }
    if (!system.start(*count_actor).has_value()) {
        return 1;
    }

    const xstate::result<void> subscribed =
        system.subscribe(*count_actor, [](const xstate::snapshot& state) {
            const boost::system::result<const boost::json::value&> count =
                state.context.try_at("count");
            if (count.has_value()) {
                std::cout << boost::json::serialize(*count) << '\n';
            }
        });
    if (!subscribed.has_value()) {
        return 1;
    }

    const xstate::event inc{.type = "INC", .payload = {}};
    const xstate::event dec{.type = "DEC", .payload = {}};
    const xstate::event set{.type = "SET", .payload = {{"value", 10}}};

    if (!system.send(*count_actor, inc).has_value()) {
        return 1;
    }
    // prints 1
    if (!system.send(*count_actor, dec).has_value()) {
        return 1;
    }
    // prints 0
    if (!system.send(*count_actor, set).has_value()) {
        return 1;
    }
    // prints 10
    return 0;
}

It prints:

1
0
10

Three things in it are xstate’s own, and each has a section of this chapter:

  • The config is JSON, and JSON holds no function. XState’s assign takes a function that computes the new count; here the config names an action, increment, and implementations binds that name to C++ code, an assign_action, as XState’s setup registers an action under a name. This is machines as JSON, implementations by name.

  • create_machine returns a result: the machine, or the reason the config was refused. Every call that can fail does the same, and the program checks each one, where XState would throw. An implementation returns a result too; when adding finds no integer count, its failure puts the actor in the status error, as an exception thrown by an assigner does in XState. This is errors as values.

  • The actor lives in an actor_system the program creates. XState’s createActor makes an actor and the system that runs it in one call; in xstate, create_actor makes an actor in a system, and start, send and subscribe are XState’s, with the actor as their first argument. The system is built with a budget of fuel, which bounds the work of each execution a call opens, one for each input the call applies; here each start and send opens one, and 100 units are far more than this counter needs. This is the actor layer.

Why xstate

A state machine names every state a process can be in and, for each state, every event it accepts and where that event leads, so a move it does not list never happens, and its logic can be read, drawn and tested as a whole. A statechart adds what larger processes need, nested and parallel states, history, delays and actions, and the actor model runs many machines together, each reaching the others only by events (Core concepts). XState brings both to JavaScript and TypeScript, and Stately Studio draws, edits and simulates XState’s machines. xstate brings the same machines to C++, for a program, native or WebAssembly, whose logic reacts to events.

A machine in xstate is data, and that is what lets XState’s machines run in C++. Its config is the JSON a person writes for XState’s createMachine, or exports from Stately Studio, read when the program runs. What JSON can say, states, transitions, guards and actions by name, delays and invokes, means in xstate what it means in XState. What JSON cannot hold, the code of an action, a guard, a delay or an invoked actor, the program registers in its implementations under the name the config gives it, as XState’s setup registers it (Machines as JSON, implementations by name). So the same machine runs on XState in JavaScript and on xstate in C++, each with implementations of its own, and What JSON imposes lists the few places where JSON gives a config another form. And a program can run machines that did not exist when it was compiled.

From such a config, xstate does what XState does:

  • It loads the machine, with the implementations the program registers by name, and refuses at once every config XState refuses (Machines).

  • It runs the machine as XState’s algorithm does: transition selection, the exit and entry sets, history, done events, eventless transitions, raised and delayed events, and output (Events and transitions).

  • It offers XState’s pure functions, initial_transition, transition and the others, which compute one snapshot from another and change nothing (Pure transitions and the macrostep cursor).

  • It returns to its caller every action the machine does not perform itself, as XState’s transition does, for the caller to execute (Custom actions).

  • It writes a machine’s definition as XState’s machine.toJSON() writes it, and loads it back (The definition).

  • It runs machines as XState’s actors: children invoked and spawned, events between actors, a child’s done and error events reaching its parent, timers, host actors, systemIds and emitted events (Actors: xstate on xactor).

It adds two things that a C++ program running machines it did not write needs, and XState does not have. Under the pure functions is the macrostep cursor, which runs one microstep per call, so that its caller bounds the work an event may cause; the actor layer bounds every call the same way, with fuel. And nothing in a run depends on the world outside it: a machine reads no clock, a delayed event waits on a clock its caller moves, and actors run on one thread in the order their messages were sent, so the same inputs give the same run (Two layers).

Why header-only

A program includes <webcpp/xstate.hpp> for machines, or <webcpp/xstate/actors.hpp> for actors, and compiles nothing of xstate’s own: there is no library to build, version, install or link. The headers are compiled with the program, by its compiler and with its flags, so a build with flags of its own, without exceptions or with a sanitizer, needs no second build of the library.

The reason is portability. The same headers build the same way wherever a C++20 compiler does, natively or as a WebAssembly component, with no build of their own to port to each target and no binary to match to each compiler. Every program of this documentation is built and run natively, with GCC, Clang and MSVC, and for wasm32-wasip2 and wasm32-wasip3 with the Clang of wasi-sdk, under wasmtime, with exceptions on every target: whether a program uses them is its own choice. xstate throws nothing, and its headers compile without them (Errors as values).

The dependencies follow the same rule. Boost.System, which gives xstate its result and its error codes, is header-only, and so are xactor and Boost.Asio, which xactor includes outside a WASI build for its Asio driver (xactor’s Asio driver). Boost.JSON is compiled by the program itself, once: one of its translation units includes <boost/json/src.hpp> (Requirements). No Boost library is built.

Two layers

XState separates a machine, the logic, from the actor that runs it: a machine’s transition computes the next snapshot and returns the actions to execute, and the actor executes them. xstate draws its line at the same place, and puts each side behind a header of its own.

Layer Header What it holds What it depends on

Machine core

<webcpp/xstate.hpp>

machines, snapshots, XState’s pure functions, the macrostep cursor, the simulated clock, a machine’s definition as JSON

the standard library, Boost.JSON and Boost.System

Actor layer

<webcpp/xstate/actors.hpp>

actor_system: actors, children, host actors, timers, the systemId registry, emitted events, subscriptions

the machine core and xactor, which includes Boost.Asio’s headers outside a WASI build

actors.hpp includes xstate.hpp, so a program that uses actors includes the one header. The machine core includes nothing of xactor and runs no actor, so a program that only steps machines does not depend on xactor. It knows XState’s actors as data all the same: implementations.actors holds the actors a machine may invoke and spawn, and snapshot.children lists its children. The entry of a state that invokes, or a spawnChild, resolves into an xstate.spawnChild action, and the exit of that state, or a stopChild, into xstate.stopChild; the core returns them, with the sends, for its caller to execute. The actor layer is that caller. The core resolves an action into a value and the layer executes it, as XState splits each action into resolve and execute.

Both layers run the same machine. This one is a dog: wakes up takes it from asleep to stretching, whose eventless transition, always, takes it on to awake, whose entry action is bark. No implementation holds bark, so it is a custom action, which the machine returns to whatever runs it:

const boost::json::value config = boost::json::parse(R"({
    "id": "dog",
    "initial": "asleep",
    "states": {
        "asleep": {"on": {"wakes up": "stretching"}},
        "stretching": {"always": "awake"},
        "awake": {"entry": "bark"}
    }
})");
const xstate::result<xstate::machine> dog = xstate::create_machine(config, {});
if (!dog.has_value()) {
    return 1;
}
const xstate::event wakes_up{.type = "wakes up", .payload = {}};

The machine core

create_machine(config, implementations) builds a machine from XState’s config. A machine never changes once it is created, and copying one is cheap. Where a machine is, its state value, its context, its status and the rest, is a snapshot, a plain value the caller keeps, copies or stores.

The core holds XState’s pure functions, each named after XState’s in snake case. get_initial_snapshot, initial_transition and get_initial_microsteps compute where a machine starts, and get_next_snapshot, transition and get_microsteps where an event takes it from a snapshot; get_next_transitions, and can, XState’s snapshot.can, say what it could do. None of them changes anything. transition, initial_transition and the microstep functions also return every action the machine does not perform itself, such as a custom action, a delayed event or a send to another actor, for the caller to execute, as XState’s transition returns them; get_next_snapshot and get_initial_snapshot return the snapshot alone.

Under those functions is the one primitive xstate adds, the macrostep cursor. An event runs one macrostep: the transitions the event selects, then every eventless transition and every raised event that follows, until the machine is stable again. Each of those steps is a microstep. XState runs a macrostep to its end inside one call. xstate’s begin(machine, snapshot, event), and begin_initial(machine, input) for the initial macrostep, return a cursor instead, whose next() runs exactly one microstep and reports whether that microstep settled the macrostep, and whose done() says when one has. The dog’s wakes up, one microstep at a time:

const xstate::snapshot asleep = xstate::get_initial_snapshot(*dog);
xstate::macrostep step = xstate::begin(*dog, asleep, wakes_up);
while (!step.done()) {
    const xstate::progress made = step.next();
    std::cout << "microstep: " << boost::json::serialize(made.step.snapshot.value) << ' '
              << types_of(made.step.actions) << '\n';
}
std::cout << "settled: " << boost::json::serialize(step.result().snapshot.value) << '\n';

It prints:

microstep: "stretching" []
microstep: "awake" [bark]
settled: "awake"

wakes up takes two microsteps: the first moves the dog to stretching, and the second takes its eventless transition to awake, whose entry action bark comes back with that microstep. XState’s getMicrosteps returns both at once; the cursor hands them over one at a time, so its caller may stop between two and resume later, since the cursor is a value and a copy of it resumes exactly where the original stopped. The caller pays for each call and stops when it chooses. The library never stops a macrostep on its own: a cycle of eventless transitions never settles, and the caller’s fuel is the only bound. The functions that compute where a machine goes are the cursor run to its end, without a bound.

The core never reads a clock either. Entering a state with after returns its delayed event as an action, a built-in xstate.raise with a delay. The caller hands it to a simulated_clock it owns, XState’s SimulatedClock, which keeps the event until the caller moves time forward and then gives it back, for the caller to send to the machine. The clock schedules only that built-in delayed raise: a custom action stays the caller’s, whatever its type or params.

Pure transitions and the macrostep cursor covers the functions, the cursor and its fuel.

The actor layer

actor_system runs machines as XState’s actors. It is XState’s createActor and its actor system in one: the host creates an actor from a machine, starts it, sends it events, reads its snapshot and subscribes to it. The machine invokes and spawns children, its sendTo, sendParent and forwardTo reach other actors, its children’s done and error events reach it, its timers fire when the host brings the time with clock_tick, and a host_actor, xstate’s counterpart of XState’s fromPromise, waits until the host resolves or rejects it. What the core returns, the layer executes: it creates the child of a spawnChild, delivers the event of a sendTo, arms a timer for a delayed raise or send, and hands a custom action to the host’s on_action. The dog, run as an actor:

xstate::actor_system system({.fuel = 10'000});
const xstate::result<void> acting =
    system.on_action([](xstate::actor_ref, const xstate::action& custom) {
        std::cout << "custom action: " << custom.type << '\n';
    });
if (!acting.has_value()) {
    return 1;
}
const xstate::result<xstate::actor_ref> dog_actor = system.create_actor(*dog);
if (!dog_actor.has_value() || !settled(system.start(*dog_actor)) ||
    !settled(system.send(*dog_actor, wakes_up))) {
    return 1;
}
const xstate::snapshot* now = system.snapshot_of(*dog_actor);
if (now == nullptr) {
    return 1;
}
std::cout << "actor: " << boost::json::serialize(now->value) << '\n';

It prints:

custom action: bark
actor: "awake"

The same machine and the same event lead to the same snapshot. The custom action bark reaches the host’s on_action, where XState’s actor calls the function setup registered under that name.

The layer runs on xactor, webcpp’s header-only actor system, which already is the system XState’s actors need (xactor’s model): a scheduler on one thread that holds one FIFO mailbox per actor and one FIFO queue of the actors that have work, and whose every turn hands one message to its actor, so that the order of deliveries depends only on the order messages were sent; fuel for each execution; timers; and a lifecycle, under XState’s names. A second actor system inside xstate would duplicate all of it. Every message an actor sends is queued and handled in a turn of its own, where XState handles an event at once, nested in the sender’s call, when its actor is running and idle (Interleaving across actors).

Fuel reaches the actors too. A host call that delivers something opens an execution for each input it applies, and resume() one for each parked actor besides; each execution has the budget’s fuel. Every microstep costs one unit of it, and so does every message an actor sends and every timer it arms. An actor that runs out parks with what it has left to do, until resume() gives it more; XState has no such bound (Fuel). Time is the host’s too: a timer fires when the host calls clock_tick, never on its own.

Actors covers the layer, its fuel and its time included.

Which to use

  • The pure functions, when the caller keeps the snapshot between events and the work of one event needs no bound: a test, or a program that loads a snapshot, applies one event and stores what it leads to.

  • The cursor, when the work of each event must be bounded, or watched one microstep at a time.

  • The actor layer, when machines invoke or spawn other machines, send each other events, wait on timers, or wait on work the host program does.

Machines as JSON, implementations by name

A config in xstate is a boost::json::value holding what XState’s createMachine takes: id, initial, context, states, on, always, after, entry, exit, invoke, tags, output and the rest. Where XState’s config holds a function, an action, a guard, a delay, the logic of an invoked actor, xstate’s config names it, and implementations maps each name to its C++ code. implementations is what XState’s setup({ actions, guards, delays, actors }) gives a machine. The counter of the first look registers its three actions this way.

There are three reasons for this:

  • A machine is data. It is loaded when the program runs, from a file, a request or a store, and needs no compiler. Its config has the shape of the one a person writes for XState’s createMachine.

  • JSON cannot hold a function, and neither can a boost::json::value: a C++ lambda has no place in the config either. A name is the bridge between the data and the code, and XState already has it: an action or a guard registered with setup is referenced by its name, with params, in the config.

  • Names are checked when the machine is created. A guard, a named delay or an actor that the config names and the implementations do not hold is refused then, with an error, where XState accepts the machine and fails later, or never, when it runs (A config is refused when it is created). An action name the implementations do not hold is not an error: it is a custom action, which the machine returns to the caller with its params. That is how the caller’s own effects leave the machine.

An action is registered as one of XState’s built-in action creators: assign_action, raise_action, log_action, send_parent_action, cancel_action, spawn_child_action, stop_child_action, send_to_action, forward_to_action and emit_action, each holding what XState’s creator takes, a C++ function where XState’s takes a JavaScript one. A guard is a predicate, and a delay a function that returns milliseconds. implementations.context computes the initial context from the input, XState’s context: ({ input }) => ..., and implementations.inputs and implementations.outputs compute an invoked child’s input and a final state’s output.

XState’s higher-order guards, not, and, or and stateIn, are functions in XState too; xstate writes them as JSON objects, {"type": "xstate.not", "guards": [...]} and the like (Higher-order guards). Machines and Actions cover each of these, and What JSON imposes lists every place where a JSON config differs from a JavaScript one.

Errors as values

xstate reports every failure as a value. Every call that can fail returns an xstate::result<T>, Boost.System’s result with an error_code; xstate’s own codes are xstate::errc, of the category webcpp.xstate. Where XState throws, xstate returns an error instead. At creation, it refuses a config XState refuses, and some that XState accepts and throws on only when it runs them, such as a guard the implementations do not hold or an entry that is null (A config is refused when it is created). While a machine runs, the error of an implementation that fails is in the snapshot’s status and error, as XState’s actor keeps the error of an implementation that threw.

There are two reasons for this. A caller reaches a value only through its result, so a failure cannot pass unseen. And xstate builds where exceptions are off: its headers compile without them, which the lint checks by compiling every public header, and every test and example, without them (Why header-only).

The actor layer refuses the calls it cannot take with xactor’s webcpp::xactor::errc::invalid_argument, of the category webcpp.xactor, and changes nothing: a start, send, stop, status_of, subscribe or on_emitted whose address names no actor, a stop of a child, an answer to a request no longer pending, and a call that changes the system made from inside a callback the host registered; xstate::errc has no invalid_argument. One refusal of the layer is xstate’s own: create_actor refuses a systemId another running actor holds with errc::system_id_taken, of the category webcpp.xstate. snapshot_of, child_of, parent_of, id_of and get refuse nothing: they answer nullptr, none or an empty view for what they do not find (Creating an actor, Options, Watching an actor).

How the port is proven

xstate’s reference is xstate@5.33.2, commit fbee62e7c1586315ed478c2fedf530d7e0ff5a3e. It is a port of XState’s algorithm, not a semantics of its own: transition selection, the exit and entry sets, history, done events, eventless transitions, raised and delayed events and output are translated function by function, and every ported function names, in its comment, the TypeScript function it ports. So a difference from XState points at one function on each side.

Where xstate departs from XState, it does so on purpose, for one of four reasons: XState’s behaviour belongs to JavaScript, JSON cannot say what XState’s config says, xactor runs actors differently, or xstate checks earlier than XState. So a config or a name is refused when the machine is created; a name stands where XState holds a function; an error is returned where XState throws; a host actor stands where XState awaits a promise; an event waits in a queue where XState delivers it to an idle actor at once, nested in its sender’s call; and fuel bounds the work XState leaves unbounded. The text says so where it matters, and Where xstate differs from XState gathers the cases, each with its reason.

A port is right when it does what the original does, so XState itself is the judge, twice. The oracle holds the library to what XState does, case by case; the twins hold this documentation to it, program by program. How xstate is tested tells how both run.

XState as the oracle

XState’s own tests are ported to cases written as JSON, one file per test file of XState’s, each with the tests it leaves out and why. Further cases pin what XState does where its own tests are silent, such as a machine whose root is a history node, an event of the type * or a timer armed twice under one id. The scripts of test/oracle/ run every case on XState 5.33.2 and record what XState produced: oracle.mjs the machine cases, one macrostep at a time through machine.transition, which XState’s pure functions call, and actors.mjs the actor cases, on XState’s actors. xstate’s test suites run the same cases and compare, microstep by microstep, the event, the transitions, the actions and the snapshot. The actor cases are compared actor by actor: the events each actor processed and the snapshot each left, the events it emitted, its status, and the host’s pending requests.

What XState produced is committed, so xstate’s suites run without Node. The oracle lane, b2 libs/xstate/test/oracle//oracle, runs the scripts on XState again, and fails when what XState produces is no longer what the suites hold.

The oracle runs XState’s development build, as XState’s own test suite does. That build makes checks the production build leaves out. Three of them can meet a JSON machine, and xstate makes them too: it refuses an event of the type * sent from outside, a transition that declares a truthy cond, and a forwardTo whose target resolves to no actor (The checks of XState’s development build).

A twin for every example

Every program of example/xstate and example/actors, the programs of this documentation that use xstate, has a twin in test/oracle/twins: a JavaScript program that does on XState 5.33.2 what the C++ program does on xstate. The oracle lane runs every twin on XState’s development build and compares what it prints with what the C++ program printed:

  • 114 twins print the same output, the one this page shows under the program.

  • 22 twins print something else, on purpose. Their program shows a place where xstate differs from XState, and the twin’s output, committed beside it, is what XState does there; Where xstate differs from XState shows both outputs, and why xstate differs.

  • 10 programs have no twin, because XState has nothing that does what they show: the macrostep cursor, fuel, a machine created again from its definition, an event read from JSON and the like. test/oracle/twins/without-twin.txt gives the reason for each.

So every program of this page that XState can run has run on XState, and printed what the page shows, or what Where xstate differs from XState explains.

What is not ported

These parts of XState 5.33.2 have no counterpart in xstate yet:

  • enqueueActions.

  • Callback actors, fromCallback, and observable actors, fromObservable and fromEventObservable.

  • fromTransition.

  • The persistence of actors: getPersistedSnapshot, and an actor created from a persisted snapshot. A machine’s snapshot is a value its caller may store, and resolve_state, XState’s machine.resolveState, brings it back; Pure transitions and the macrostep cursor shows it.

  • toPromise and waitFor.

  • syncSnapshot, and with it an invoke’s onSnapshot: the config accepts onSnapshot, but the actor layer sends no child’s snapshot to its parent, so no child’s snapshot ever takes that transition.

  • spawn inside assign.

  • machine.provide(): a machine takes its implementations once, from create_machine, and the same config created again with other implementations is the machine provide would return; Machines shows it.

xactor on its own leaves four things to the library built on it (what xactor leaves out): a child’s done and error reaching its parent, invoke, a registry of named actors, and the inspection of state changes. xstate’s actor layer builds all four for machines (Actors: xstate on xactor).

Requirements

  • A C++20 compiler. xstate’s tests and examples are built and run natively, and for wasm32-wasip2 and wasm32-wasip3 with the Clang of wasi-sdk, under wasmtime (Targets).

  • xstate’s headers and xactor’s, libs/xstate/include and libs/xactor/include in a webcpp checkout. The machine core needs only xstate’s; the actor layer needs xactor’s too. Nothing of xstate is compiled on its own (Why header-only).

  • webcpp’s configuration, libs/config/include in a webcpp checkout, which xstate’s and xactor’s headers include first.

  • Boost 1.92.0’s headers. Every value a machine holds, its config, its context, an event’s payload, an action’s params, is a Boost.JSON value; results and error codes are Boost.System’s. Outside a WASI build, xactor includes Boost.Asio’s headers for its Asio driver.

  • Boost.JSON’s definitions, compiled once. Boost.JSON’s headers declare and <boost/json/src.hpp> defines, so exactly one translation unit of the program includes that header; webcpp’s is tools/boost_json.cpp.

  • Without exceptions, a definition of boost::throw_exception. xstate throws nothing: it is built and tested with exceptions on, on every target, and its headers compile without them. Whether a program uses them is its own choice. Without them, Boost’s code still names boost::throw_exception, and the program defines it: webcpp’s tools/throw_exception.cpp does it with a report on the standard error and an abort, since in such a build a throw is a defect, never a condition. A program built with exceptions needs nothing here, since Boost defines the function then.

  • Nothing else: no threads, and no global or static mutable state. A machine never reads a clock, a file or the network: the same machine and the same events give the same run.

Installing xstate sets this up, and compiles a first program.

Quick start

This section builds a first program. It creates a machine, a toggle, from the JSON config XState’s createMachine takes, steps it with XState’s pure functions, runs the same machine as an actor that receives events, and then grows it with a delayed transition, context and input. It follows Stately’s quick start step by step, with one step of its own, the pure functions. Each block of code comes from a whole program of the repository, and the output under it is what that program printed.

Installing xstate

xstate is header-only, so installing it puts directories on a program’s include path, and nothing is built but the program (Requirements lists what the program provides). In a webcpp checkout they are libs/xstate/include, webcpp’s configuration, libs/config/include, and, for the actor layer, libs/xactor/include, beside Boost 1.92’s headers. From the root of the checkout, with the program of the next two sections, libs/xstate/example/xstate/qs_toggle.cpp, copied as toggle.cpp, this command builds it:

clang++ -std=c++20 -I libs/xstate/include -I libs/config/include toggle.cpp \
    tools/boost_json.cpp -o toggle

tools/boost_json.cpp is the one translation unit that includes <boost/json/src.hpp>; without it the link misses Boost.JSON’s definitions. A compiler that does not find Boost on its default include path is given Boost’s include directory with -isystem, which makes Boost’s headers system headers, so the compiler’s warnings stay on the program’s own code: for Homebrew’s Boost, the one under /opt/homebrew/opt/boost. A program that uses the actor layer adds -I libs/xactor/include. GCC takes the same options.

Without exceptions, the program also needs a definition of boost::throw_exception, which Boost.System and Boost.JSON name: webcpp’s tools/throw_exception.cpp defines its two overloads, reports what was thrown on the standard error and aborts, since in such a build a throw is a defect, never a condition:

clang++ -std=c++20 -fno-exceptions -DBOOST_NO_EXCEPTIONS -I libs/xstate/include \
    -I libs/config/include toggle.cpp tools/boost_json.cpp tools/throw_exception.cpp \
    -o toggle

A program built with exceptions on needs none of -fno-exceptions, -DBOOST_NO_EXCEPTIONS or throw_exception.cpp, since Boost defines the function itself then.

Building with b2

In the checkout, b2 libs/xstate/example builds this program with every other program of libs/xstate/example/, runs each, and compares what it prints with the output this page shows, which is the <name>.expected beside it (Testing). A b2 project uses xstate through /webcpp/xstate//xstate, which adds xstate’s headers, xactor’s, Boost’s, and Boost.JSON’s definitions, compiled once per variant, to whatever names it:

exe toggle : toggle.cpp /webcpp/xstate//xstate ;

Create a machine

A machine is the logic of an actor: the states it can be in, the events each state accepts, and where each event leads. This one, a toggle, is in one of two states, Inactive or Active, and the event toggle moves it from one to the other.

XState takes the config as a JavaScript object. xstate takes the same config as JSON, read at run time by create_machine: a C++ program has no object literal to write it in, and as data a config can come from a file, a request or Stately Studio’s export. JSON cannot hold a function, so the program gives the machine’s code in the second argument, the implementations: an action’s, for one, under the name the config gives the action. This config needs no code, and its implementations are empty (Handle context data, Implementations).

The examples of this page begin with the core’s header, Boost.JSON’s, and an alias for the namespace:

#include <webcpp/xstate.hpp>

#include <boost/json.hpp>

#include <iostream>

namespace xstate = webcpp::xstate;

Then the machine:

const boost::json::value config = boost::json::parse(R"({
    "id": "toggle",
    "initial": "Inactive",
    "states": {
        "Inactive": {"on": {"toggle": "Active"}},
        "Active": {"on": {"toggle": "Inactive"}}
    }
})");
const xstate::result<xstate::machine> toggle_machine = xstate::create_machine(config, {});
if (!toggle_machine.has_value()) {
    std::cout << "refused: " << toggle_machine.error().message() << '\n';
    return 1;
}

create_machine returns a result<machine>: the machine, or the error that refused the config, such as invalid_config for a compound state without initial or unknown_target for a target that names no state. Where XState throws, xstate returns an error; and some configs XState accepts, failing only when it runs them, xstate refuses when the machine is created (A config is refused when it is created). A machine never changes once created, and copying one is cheap: it is a handle to data its copies share. What creation refuses tells what refuses a config.

Step it with pure functions

Before it runs as an actor, the machine can be stepped by hand, which suits a caller that keeps the state itself: a request handler that loads a snapshot, applies an event and stores the result, or a test. A snapshot is where a machine is: its state value, its context and its status. XState’s pure functions compute one snapshot from another. initialTransition returns the snapshot a machine starts in, and transition the snapshot an event leads to, each with the actions the step returned for the caller to execute. They change nothing and keep nothing: the caller holds the snapshot and passes it back with the next event. xstate has the same functions, initial_transition and transition, which return a std::pair:

const auto [initial_state, initial_actions] = xstate::initial_transition(*toggle_machine);
std::cout << "Value: " << boost::json::serialize(initial_state.value) << " ("
          << initial_actions.size() << " actions)\n";

const xstate::event toggle{.type = "toggle", .payload = {}};
const auto [next_state, actions] = xstate::transition(*toggle_machine, initial_state, toggle);
std::cout << "Value: " << boost::json::serialize(next_state.value) << " (" << actions.size()
          << " actions)\n";

const auto [last_state, last_actions] = xstate::transition(*toggle_machine, next_state, toggle);
std::cout << "Value: " << boost::json::serialize(last_state.value) << " ("
          << last_actions.size() << " actions)\n";

It prints:

Value: "Inactive" (0 actions)
Value: "Active" (0 actions)
Value: "Inactive" (0 actions)

An event is a type and a payload, empty here. The toggle has no actions, so every list is empty; Actions shows what they hold and who executes them.

An event’s macrostep is all the work it causes: one or more microsteps, each a set of transitions taken together, until no eventless transition is enabled and no raised event is queued (Microsteps). Each of these functions runs an event’s macrostep to its end at once. Under them is xstate’s one primitive, the macrostep cursor: begin returns a cursor whose next() runs exactly one microstep, so the caller decides how much work an event may cost, paying for each microstep and stopping where it wants. A machine whose eventless transitions never settle never returns from transition; only the cursor bounds it (The macrostep cursor).

Create an actor and send events

An actor is a running process: it receives events, one at a time, and each event may change its snapshot and make it send events of its own. A machine is the logic an actor runs.

XState’s createActor makes an actor from a machine. In xstate, actors live in an actor_system, which runs them on xactor, and the program names each by the actor_ref the system returns. XState makes a system for each root actor implicitly; xstate makes it explicit because the system owns what a host must decide: the fuel its actors may spend, the time, and the systemIds its actors share (Creating an actor). xactor runs the actors on one thread: each actor has a FIFO mailbox, the actors that have work wait in one FIFO queue, and each turn hands one message to its actor, so the order of deliveries depends on nothing but the order the messages were sent (xactor’s guarantee 2). With fuel bounding each execution, the same inputs make the same run and no run is unbounded. The system’s create_actor, subscribe, start and send are XState’s createActor, actor.subscribe, actor.start and actor.send, and its snapshot_of is actor.getSnapshot. An actor needs the actor layer’s header, <webcpp/xstate/actors.hpp>. The same toggle, as an actor:

xstate::actor_system system({.fuel = 1000});

/** Whether a call that delivers ran to its end: no error, and no actor left parked. */
const auto settled = [](const xstate::result<xstate::run_outcome>& ran) {
    return ran.has_value() && *ran == xstate::run_outcome::settled;
};

const xstate::result<xstate::actor_ref> actor = system.create_actor(*toggle_machine);
if (!actor.has_value()) {
    return 1;
}

const xstate::result<void> subscribed =
    system.subscribe(*actor, [](const xstate::snapshot& snapshot) {
        std::cout << "Value: " << boost::json::serialize(snapshot.value) << '\n';
    });
if (!subscribed.has_value()) {
    return 1;
}

if (!settled(system.start(*actor))) {  // logs "Inactive"
    return 1;
}

const xstate::event toggle{.type = "toggle", .payload = {}};
if (!settled(system.send(*actor, toggle))) {  // logs "Active"
    return 1;
}
if (!settled(system.send(*actor, toggle))) {  // logs "Inactive"
    return 1;
}

const xstate::snapshot* now = system.snapshot_of(*actor);
if (now == nullptr) {
    return 1;
}
std::cout << "Now: " << boost::json::serialize(now->value) << '\n';

It prints:

Value: "Inactive"
Value: "Active"
Value: "Inactive"
Now: "Inactive"
  • A call runs the system. start and send, like every call that delivers something, run the system until nothing is left to do before they return, as XState’s send processes the event at once, unless an actor is parked: then the call is held, in order, until resume(). Each returns a result<run_outcome>: settled when no actor is left parked, out_of_fuel when one is, or an error, such as xactor’s invalid_argument, of the category webcpp.xactor, for an actor the system does not know (Errors as values).

  • Fuel. The budget the system is created with, {.fuel = 1000}, is the fuel of each execution a call opens; a start or a send opens one. Every microstep costs one unit, and so does every message an actor sends and every timer it arms. When an execution runs out, the call returns out_of_fuel and leaves the actor parked, its work kept and its snapshot the last one it settled in; resume() continues it. XState has no fuel, and an actor whose eventless transitions never settle runs forever there (Fuel, parking and resume, Fuel). The example’s settled takes an error and out_of_fuel alike as a failure, and returns 1.

  • Start. start constructs the actor and runs its initial macrostep, so snapshot_of returns nullptr until then. XState’s createActor already constructs it, and its getSnapshot() answers before start() (The root is constructed on start).

  • Subscribers. A subscriber hears each snapshot the actor settles in while it is active, and the one it is done in, whether the event took a transition or not, as XState’s does. It runs inside the actor’s turn: it may read the system, with snapshot_of for one, but a call from it that changes the system, a send included, returns invalid_argument and changes nothing, where XState accepts a send from a subscriber and handles it once the update in progress is done (Watching an actor, A callback that calls the system back).

Actors covers the rest: children, messages between actors, actors the host answers for, time, and what fuel guarantees.

Use delayed transitions

A delayed transition is taken when its state has been active for a given time, with no event. A state lists its delayed transitions under after, keyed by the delay in milliseconds. Here Active goes back to Inactive two seconds after it was entered, unless a toggle leaves it first, which cancels the delay.

The machine never reads a clock. Entering Active arms a timer, which waits until the host brings the time: clock_tick(2000) moves the system’s time to 2000 milliseconds and fires the timers due by then. XState’s actors schedule delays with JavaScript’s setTimeout unless they are given another clock, such as its SimulatedClock; an xstate actor has no clock but the host’s (In the actor layer, Delayed events and time).

const boost::json::value config = boost::json::parse(R"({
    "id": "toggle",
    "initial": "Inactive",
    "states": {
        "Inactive": {"on": {"toggle": "Active"}},
        "Active": {
            "on": {"toggle": "Inactive"},
            "after": {"2000": "Inactive"}
        }
    }
})");
const xstate::result<xstate::machine> toggle_machine = xstate::create_machine(config, {});
if (!toggle_machine.has_value()) {
    return 1;
}

xstate::actor_system system({.fuel = 1000});
const auto settled = [](const xstate::result<xstate::run_outcome>& ran) {
    return ran.has_value() && *ran == xstate::run_outcome::settled;
};

const xstate::result<xstate::actor_ref> actor = system.create_actor(*toggle_machine);
if (!actor.has_value()) {
    return 1;
}

const xstate::result<void> subscribed =
    system.subscribe(*actor, [](const xstate::snapshot& snapshot) {
        std::cout << "Value: " << boost::json::serialize(snapshot.value) << '\n';
    });
if (!subscribed.has_value()) {
    return 1;
}

if (!settled(system.start(*actor))) {  // logs "Inactive"
    return 1;
}
const xstate::event toggle{.type = "toggle", .payload = {}};
if (!settled(system.send(*actor, toggle))) {  // logs "Active"
    return 1;
}
if (!settled(system.clock_tick(1000))) {  // logs nothing
    return 1;
}
if (!settled(system.clock_tick(2000))) {  // logs "Inactive"
    return 1;
}

It prints:

Value: "Inactive"
Value: "Active"
Value: "Inactive"

At 1000 milliseconds the timer is not due, and nothing happens. At 2000 it fires. An after is an event the state raises with a delay when it is entered, xstate.after.2000.toggle.Active here, and cancels when it is left; the delayed transition is a transition on that event, as in XState (In the machine core).

Handle context data

Context is the data a machine carries beside its state: here, how many times the toggle became Active. A context that is data stays in the config, as JSON: "context": {"count": 0} is XState’s context: { count: 0 }, and it is the context the machine starts with. An assign action updates it: on entering Active, incrementCount adds one to the count.

In XState the update is a function written inline in the config, which JSON cannot hold. In xstate the config names the action, and the program registers it under that name in implementations.actions, as XState’s setup({ actions }) registers implementations by name (Implementations). An assign_action receives an action_args, the context, the event and the action’s params, and returns the members it changes, which it merges into the context, as XState’s assign does (Assign).

An implementation returns a result. A failure stops the macrostep and leaves the snapshot it began from, its status error, where XState’s function would throw (Failures are values).

const boost::json::value config = boost::json::parse(R"({
    "id": "toggle",
    "context": {"count": 0},
    "initial": "Inactive",
    "states": {
        "Inactive": {"on": {"toggle": "Active"}},
        "Active": {
            "entry": "incrementCount",
            "on": {"toggle": "Inactive"},
            "after": {"2000": "Inactive"}
        }
    }
})");

xstate::implementations implementations;
implementations.actions.emplace(
    "incrementCount",
    xstate::assign_action{
        .assignment =
            [](const xstate::action_args& args) -> xstate::result<boost::json::object> {
            const std::optional<std::int64_t> count = integer_at(args.context, "count");
            if (!count.has_value()) {
                return xstate::failure<boost::json::object>(
                    xstate::errc::implementation_failed);
            }
            return boost::json::object{{"count", *count + 1}};
        },
    });

const xstate::result<xstate::machine> toggle_machine =
    xstate::create_machine(config, implementations);
if (!toggle_machine.has_value()) {
    return 1;
}

xstate::actor_system system({.fuel = 1000});
const auto settled = [](const xstate::result<xstate::run_outcome>& ran) {
    return ran.has_value() && *ran == xstate::run_outcome::settled;
};

const xstate::result<xstate::actor_ref> actor = system.create_actor(*toggle_machine);
if (!actor.has_value()) {
    return 1;
}

const xstate::result<void> subscribed =
    system.subscribe(*actor, [](const xstate::snapshot& snapshot) {
        std::cout << "State: " << boost::json::serialize(snapshot.value) << ' '
                  << boost::json::serialize(snapshot.context) << '\n';
    });
if (!subscribed.has_value()) {
    return 1;
}

const xstate::event toggle{.type = "toggle", .payload = {}};
if (!settled(system.start(*actor))) {
    return 1;
}
if (!settled(system.send(*actor, toggle))) {  // Active, count 1
    return 1;
}
if (!settled(system.clock_tick(2000))) {  // Inactive, count 1
    return 1;
}
if (!settled(system.send(*actor, toggle))) {  // Active, count 2
    return 1;
}

It prints:

State: "Inactive" {"count":0}
State: "Active" {"count":1}
State: "Inactive" {"count":1}
State: "Active" {"count":2}

The action reads the count with integer_at, which returns none where a member is missing or is not an integer. Boost.JSON’s accessors that check, such as at and as_int64, throw on a mismatch, and a throw aborts a program built without exceptions; if_object, if_contains and is_int64 answer with a pointer or a bool instead, and try_at and try_as_int64 with a boost::system::result:

/** The integer `key` holds in the object `json`; none when it holds no integer. */
std::optional<std::int64_t> integer_at(const boost::json::value& json, std::string_view key) {
    const boost::json::object* object = json.if_object();
    if (object == nullptr) {
        return std::nullopt;
    }
    const boost::json::value* member = object->if_contains(key);
    if (member == nullptr || !member->is_int64()) {
        return std::nullopt;
    }
    return member->get_int64();
}

Context covers context one part at a time: its initial value, assign, how guards and actions read it, and how an actor holds it.

Add input

Input is the data an actor is created with. Here it is the most times the toggle may become Active, maxCount, and the initial context is computed from it. A guard is a condition a transition is taken under: countBelowMax lets toggle activate the toggle only while the count is below that maximum.

In XState both are functions written in the config. In xstate the config names the guard, and the program registers it under that name in implementations.guards, as it registers the action: a predicate of an action_args that returns a result<bool> (Guards). The context function, which a config cannot name, goes in implementations.context: it receives the input and returns the initial context, a result<boost::json::value>, in place of the config’s context. A failure it returns, or a value that is not an object, fails the actor’s initial macrostep and leaves it in the status error (Initial context from the input, A context that is not an object). The input is the input of the actor_options given to create_actor, as XState’s is an option of createActor:

const boost::json::value config = boost::json::parse(R"({
    "id": "toggle",
    "initial": "Inactive",
    "states": {
        "Inactive": {
            "on": {
                "toggle": {"guard": "countBelowMax", "target": "Active"}
            }
        },
        "Active": {
            "entry": "incrementCount",
            "on": {"toggle": "Inactive"},
            "after": {"2000": "Inactive"}
        }
    }
})");

xstate::implementations implementations;
implementations.context =
    [](const boost::json::value& input) -> xstate::result<boost::json::value> {
    const std::optional<std::int64_t> max_count = integer_at(input, "maxCount");
    if (!max_count.has_value()) {
        return xstate::failure<boost::json::value>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"count", 0}, {"maxCount", *max_count}};
};
implementations.guards.emplace(
    "countBelowMax", [](const xstate::action_args& args) -> xstate::result<bool> {
        const std::optional<std::int64_t> count = integer_at(args.context, "count");
        const std::optional<std::int64_t> max_count = integer_at(args.context, "maxCount");
        if (!count.has_value() || !max_count.has_value()) {
            return xstate::failure<bool>(xstate::errc::implementation_failed);
        }
        return *count < *max_count;
    });
implementations.actions.emplace(
    "incrementCount",
    xstate::assign_action{
        .assignment =
            [](const xstate::action_args& args) -> xstate::result<boost::json::object> {
            const std::optional<std::int64_t> count = integer_at(args.context, "count");
            if (!count.has_value()) {
                return xstate::failure<boost::json::object>(
                    xstate::errc::implementation_failed);
            }
            return boost::json::object{{"count", *count + 1}};
        },
    });

const xstate::result<xstate::machine> toggle_machine =
    xstate::create_machine(config, implementations);
if (!toggle_machine.has_value()) {
    return 1;
}

xstate::actor_system system({.fuel = 1000});
const auto settled = [](const xstate::result<xstate::run_outcome>& ran) {
    return ran.has_value() && *ran == xstate::run_outcome::settled;
};

const xstate::actor_options options{
    .input = boost::json::object{{"maxCount", 1}},
    .id = std::nullopt,
    .system_id = std::nullopt,
};
const xstate::result<xstate::actor_ref> actor = system.create_actor(*toggle_machine, options);
if (!actor.has_value()) {
    return 1;
}

const xstate::result<void> subscribed =
    system.subscribe(*actor, [](const xstate::snapshot& snapshot) {
        std::cout << "State: " << boost::json::serialize(snapshot.value) << ' '
                  << boost::json::serialize(snapshot.context) << '\n';
    });
if (!subscribed.has_value()) {
    return 1;
}

const xstate::event toggle{.type = "toggle", .payload = {}};
if (!settled(system.start(*actor))) {
    return 1;
}
if (!settled(system.send(*actor, toggle))) {  // Active, count 1
    return 1;
}
if (!settled(system.clock_tick(2000))) {  // after 2000 ms: Inactive
    return 1;
}
if (!settled(system.send(*actor, toggle))) {  // count is not below maxCount: stays
    return 1;
}

It prints:

State: "Inactive" {"count":0,"maxCount":1}
State: "Active" {"count":1,"maxCount":1}
State: "Inactive" {"count":1,"maxCount":1}
State: "Inactive" {"count":1,"maxCount":1}

The last toggle finds a count of 1, not below a maxCount of 1, so the guard refuses it and the snapshot stays; the subscriber hears it all the same. Input and output covers input, and output, the data an actor is done with.

How to read this documentation

The page goes from a first program to every name in the library. After this introduction, each chapter takes one part, and the appendices gather what a reader looks up:

  • State machines takes a machine one concept at a time, as Stately’s documentation does: the core concepts, machines, states, context, transitions, actions, input and output, and then the pure functions, the macrostep cursor and fuel.

  • Actors: xstate on xactor runs machines as actors: creating them, invoking and spawning children, the messages between actors, the system, time, host actors and fuel.

  • Testing tests machines and actors, and tells how xstate itself is tested against XState.

  • Guarantees lists what xstate promises whatever its caller does, each promise under a number that the code and the tests cite.

  • Reference lists every name xstate declares, from the Doc Comments of its headers.

  • From XState to C++ shows how each of XState’s JavaScript spellings is written in C++.

  • Where xstate differs from XState lists every place where xstate departs from XState, each with its reason.

  • Rationale gives the design decisions, each with its reason.

  • Glossary defines the page’s terms.

Every program on the page is compiled and run by b2 libs/xstate/example, which compares what it prints with the output committed beside it, and the page shows the program’s source and that output, so neither can drift from the library. Every program that uses xstate, and does what XState can do, also runs on XState, as its twin (A twin for every example).

The C++ examples write namespace xstate = webcpp::xstate;. Names follow XState’s, in snake case: createMachine is create_machine, getNextSnapshot is get_next_snapshot, and setup({ actions }) is implementations. An actor is named by the actor_ref its system returned, so XState’s actor.send(event) is system.send(actor, event). An event is xstate::event{.type, .payload}, XState’s { type, ...payload }. From XState to C++ gives each of these spellings, with its reason.

xactor, which the actor layer runs on, is taught alone on its own page: its scheduler, turns, fuel, timers and drivers. This page names xactor where the actor layer leans on it, and links that page there.

State machines

A machine in xstate is the logic of one process: the states it can be in, the events it reacts to, where each event takes it and what it does on the way. It is built from two things:

  • a config, the JSON object XState’s createMachine takes, read when the program runs, from a file, a request or a string in the code;

  • implementations, the C++ code of the actions, guards, delays and actors the config names, registered under those names.

create_machine(config, implementations) checks the two together and builds a machine, an immutable value: it never changes, and its copies share its data. A machine does not hold where it is either. That is a snapshot, a plain value its caller keeps, so one machine serves any number of snapshots and runs at once.

A machine runs in one of two ways. Its caller can step it with the pure functions, XState’s initialTransition, transition and their relatives, each of which computes the next snapshot from a snapshot and an event and returns the actions for the caller to execute; under them, the macrostep cursor runs one microstep per call, so that the caller bounds the work of each event. The last section of this chapter, Pure transitions and the macrostep cursor, covers both. Or an actor runs it: the actor keeps the snapshot, executes the actions, runs children and exchanges events with other actors, which is the subject of the next chapter, Actors: xstate on xactor. This chapter’s examples use the pure functions, which show what a machine does one event at a time and need nothing else.

The sections go from the ideas to the details. Core concepts explains the parts of a statechart in plain words, with one small example each. Machines shows how a machine is created, what its config holds and how its implementations are registered. States, Context, Events and transitions, Actions and Input and output then take each part of a config in depth, and Pure transitions and the macrostep cursor the functions that run it.

Core concepts

A statechart describes what one process does when something happens: the states it can be in, the events it reacts to, and what each event changes. This section explains its parts in plain words, one part at a time, each with the smallest program that shows it, and the sections after it take each part in depth. The other idea xstate rests on, the actor model, which describes how many such processes live together, each reaching the others only by events, opens the next chapter (The actor model).

The examples follow a dog through its day, as Stately’s introduction does. Each one runs its machine with two of the core’s pure functions: get_initial_snapshot gives where the machine is when it starts, and get_next_snapshot where an event takes it from there.

State machines and statecharts

A state machine models how a process goes from state to state when an event occurs. It lists every state the process can be in, every event it reacts to, and, for each state and event, where the process goes next.

Writing this down makes the impossible states visible, since a state that is not listed cannot happen, and the unwanted moves too, since a move that is not listed is never taken. The same state and the same event always lead to the same next state, so a machine is simple to test.

A statechart is a state machine extended for larger processes: a state can contain states (hierarchy), several states can be active at once (parallel states), a state can remember where it was (history), and the process carries data beside its state (context). XState implements statecharts, and xstate runs the same ones, from the same config written as JSON, with a name wherever XState’s config would hold a function (Machines as JSON, implementations by name). What is not ported lists what it does not run yet, and Where xstate differs from XState where it differs. As in Stately’s documentation, "machine" here means a statechart.

In xstate, a machine is the tree of state nodes its config describes, together with the implementations the config names, and it never changes once it is created. Where the machine is at one moment is a separate value, its snapshot (The snapshot). Each event computes a new snapshot from the previous one, and the machine stays as it was.

States

A state is a mode the process is in, such as asleep or awake. Without parallel states (Parallel states), a machine is in exactly one state at each level at a time: a dog is asleep or awake, never both and never neither. There are finitely many of them, which is why such a machine is called a finite state machine.

A config lists the states as the members of states, and names with initial the one the machine enters when it starts:

const boost::json::value config = boost::json::parse(R"({
    "id": "dog",
    "initial": "asleep",
    "states": {
        "asleep": {},
        "awake": {}
    }
})");
const xstate::result<xstate::machine> dog = xstate::create_machine(config, {});
if (!dog.has_value()) {
    return 1;
}

const xstate::snapshot first = xstate::get_initial_snapshot(*dog);
std::cout << "value: " << boost::json::serialize(first.value) << '\n';
std::cout << std::boolalpha << "asleep: " << first.matches("asleep") << '\n';
std::cout << "awake: " << first.matches("awake") << '\n';

It prints:

value: "asleep"
asleep: true
awake: false

create_machine reads the JSON config, the object XState’s createMachine takes, with the implementations the config names, none here, so {}. It returns a result: the machine, or the error that refused the config and no machine. A config that cannot be a machine is refused at once, so the program checks the result before it uses the machine (What creation refuses). get_initial_snapshot returns where the machine is when it starts, its snapshot, whose value names the current state, and matches asks whether the snapshot is in a given state. get_initial_snapshot, and get_next_snapshot below, are the short forms of initial_transition and transition, XState’s initialTransition and transition, that return the snapshot alone (The snapshot alone).

A machine whose root has states of its own, and is not parallel, names one initial state, and only one, or it would not know where to start: create_machine refuses such a config when it leaves initial out, as XState’s createMachine refuses it. A parent state (Parent states) names its initial child the same way, under the same rule. Two kinds of state need no initial: one without children, such as asleep, and a parallel state, the root included, which enters all its children at once (Parallel states). Creating a machine says more about create_machine, Initial states about the initial state, and Matching a state value about matches.

Events and transitions

An event is something that happened: the dog wakes up, the dog falls asleep. It has a type, a string, and may carry data. A transition is a move from one state, its source, to another, its target, when an event of a given type happens. Transitions are what make a machine react: for each state, they say which events matter there and where each one leads. A state lists its transitions in on, by event type:

const boost::json::value config = boost::json::parse(R"({
    "id": "dog",
    "initial": "asleep",
    "states": {
        "asleep": {"on": {"wakes up": "awake"}},
        "awake": {"on": {"falls asleep": "asleep"}}
    }
})");
const xstate::result<xstate::machine> dog = xstate::create_machine(config, {});
if (!dog.has_value()) {
    return 1;
}

xstate::snapshot now = xstate::get_initial_snapshot(*dog);
for (const std::string_view type : {"wakes up", "wakes up", "falls asleep"}) {
    const xstate::event happened{.type = std::string(type), .payload = {}};
    now = xstate::get_next_snapshot(*dog, now, happened);
    std::cout << type << " -> " << boost::json::serialize(now.value) << '\n';
}

It prints:

wakes up -> "awake"
wakes up -> "awake"
falls asleep -> "asleep"

get_next_snapshot computes the next snapshot from a snapshot and an event, and changes neither. The second wakes up arrives in awake, which has no transition for it, so the dog stays awake: when neither the current state nor any state that contains it has a transition for an event, the event changes nothing, and the snapshot stays as it was.

An xstate::event holds the type and a JSON object, its payload. XState’s event is one flat object, { type: 'wakes up', ...payload }, and event_from_json reads that form. Events and transitions each have a section of their own, Events and Transitions.

One event can cause more than one step: after its transition, the machine may take eventless transitions (always) and events it raised itself. Each such step is a microstep; all of them, until the machine is stable, are one macrostep, the unit in which a machine handles an event. get_next_snapshot runs a whole macrostep in one call, as XState does. The macrostep cursor under it runs one microstep per call instead, so that its caller decides how much work an event may cost (The machine core, The macrostep cursor).

Actions

An action is an effect: something the machine does as it moves, such as notifying, logging or saving, as opposed to where it goes. The machine says when an effect happens; the code that performs it lives outside the states and transitions. Most actions are fire and forget: the machine neither waits for them nor reads their result. The exception is the built-in assign, whose result the machine merges into its context (Context). A state has entry actions, run when it is entered, and exit actions, run when it is left; a transition has actions of its own, run as it is taken:

const boost::json::value config = boost::json::parse(R"({
    "id": "dog",
    "initial": "asleep",
    "states": {
        "asleep": {
            "exit": "stretch",
            "on": {"wakes up": {"target": "awake", "actions": "yawn"}}
        },
        "awake": {"entry": {"type": "bark", "params": {"times": 2}}}
    }
})");
const xstate::result<xstate::machine> dog = xstate::create_machine(config, {});
if (!dog.has_value()) {
    return 1;
}

const xstate::snapshot asleep = xstate::get_initial_snapshot(*dog);
const xstate::event wakes_up{.type = "wakes up", .payload = {}};
const auto [awake, actions] = xstate::transition(*dog, asleep, wakes_up);
std::cout << "value: " << boost::json::serialize(awake.value) << '\n';
for (const xstate::action& returned : actions) {
    std::cout << "the caller executes " << returned.type;
    if (!returned.params.is_null()) {
        std::cout << ' ' << boost::json::serialize(returned.params);
    }
    std::cout << '\n';
}

It prints:

value: "awake"
the caller executes stretch
the caller executes yawn
the caller executes bark {"times":2}

In one move the exit actions come first, then the transition’s, then the entry actions: stretch, yawn, bark. A JSON config cannot hold a function, so in xstate an action is a name, with optional params, where XState’s config may also hold the function itself (Functions become names). What the name does is decided outside the config. stretch, yawn and bark have no implementation: they are custom actions, and transition returns them, in order and with their params, for the caller to execute, as XState’s pure transition returns every action and executes none.

The built-in actions, XState’s assign, raise, sendTo and the others, are registered by name in implementations, and xstate resolves them: assign changes the context and is never returned, a raise without a delay queues its event in the macrostep, and every built-in but assign comes back resolved, under XState’s name (xstate.raise, xstate.sendTo, xstate.spawnChild and so on), for the actor layer or the caller to execute. Built-in and custom actions lists them all, and Custom actions covers custom actions.

Context

The finite states say which mode the process is in; they cannot count, or hold a name. Context holds that data: a JSON object that travels with the state, the extended state of a statechart. Only an assign action changes it, by merging the object its implementation returns into the context, as JavaScript’s Object.assign does, so every change to the data is an action the config names. Here each gets treat runs eatTreat, which counts the treat:

/** One more treat: the partial context `eatTreat` merges into the context. */
xstate::result<boost::json::object> eat_treat(const xstate::action_args& args) {
    const boost::json::object* context = args.context.if_object();
    const boost::json::value* treats =
        context == nullptr ? nullptr : context->if_contains("treats");
    if (treats == nullptr || !treats->is_int64()) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"treats", treats->get_int64() + 1}};
}
xstate::implementations implementations;
implementations.actions.emplace("eatTreat", xstate::assign_action{.assignment = eat_treat});

const boost::json::value config = boost::json::parse(R"({
    "id": "dog",
    "initial": "begging",
    "context": {"treats": 0},
    "states": {
        "begging": {"on": {"gets treat": {"actions": "eatTreat"}}}
    }
})");
const xstate::result<xstate::machine> dog = xstate::create_machine(config, implementations);
if (!dog.has_value()) {
    return 1;
}

xstate::snapshot now = xstate::get_initial_snapshot(*dog);
print(now);
const xstate::event gets_treat{.type = "gets treat", .payload = {}};
for (int treat = 0; treat < 2; ++treat) {
    now = xstate::get_next_snapshot(*dog, now, gets_treat);
    print(now);
}

It prints:

"begging" {"treats":0}
"begging" {"treats":1}
"begging" {"treats":2}

eat_treat computes the update: it is the function XState’s assign takes. An assign_action holds it, registered in implementations.actions under the name the config uses, eatTreat, as XState’s setup registers it. It receives an action_args: the context as it is when the action runs, the event, and the action’s params. It returns a result: when it cannot compute the update, here when treats is missing or not an integer, it returns an error rather than throwing, and the macrostep fails, leaving the snapshot it began from with the status error.

The transition on gets treat has no target, so the dog stays begging and only the action runs. Stately calls this a self-transition: doing something, not going somewhere. Without a target, the state is neither exited nor entered again (Self-transitions and reenter).

A config without context, or with null, starts from {}, as in XState. Any other context that is not an object, false, a number, a string or a list, is kept as written, and an assign on it fails with implementation_failed, where XState starts a falsy one from {} and makes any other an object at its first assign (A context that is not an object). Updating context with assign covers assign, and Initial context from the input how an input sets the first context.

Parent states

A parent state, or compound state, contains states of its own, its children, and is active while one of them is. A state without children is atomic. Parents exist so that what several states share is written once: a transition of the parent applies whichever child is active. The dog arrives home from walking or from running, and the transition is written once, on onAWalk, which names its initial child, entered with it:

const boost::json::value config = boost::json::parse(R"({
    "id": "walk",
    "initial": "waiting",
    "states": {
        "waiting": {"on": {"leave home": "onAWalk"}},
        "onAWalk": {
            "initial": "walking",
            "states": {
                "walking": {"on": {"speed up": "running"}},
                "running": {"on": {"slow down": "walking"}}
            },
            "on": {"arrive home": "walkComplete"}
        },
        "walkComplete": {}
    }
})");
const xstate::result<xstate::machine> walk = xstate::create_machine(config, {});
if (!walk.has_value()) {
    return 1;
}

xstate::snapshot now = xstate::get_initial_snapshot(*walk);
for (const std::string_view type : {"leave home", "speed up", "arrive home"}) {
    const xstate::event happened{.type = std::string(type), .payload = {}};
    now = xstate::get_next_snapshot(*walk, now, happened);
    std::cout << type << " -> " << boost::json::serialize(now.value) << std::boolalpha
              << ", on a walk: " << now.matches("onAWalk") << '\n';
}

It prints:

leave home -> {"onAWalk":"walking"}, on a walk: true
speed up -> {"onAWalk":"running"}, on a walk: true
arrive home -> "walkComplete", on a walk: false

The state value of a parent state is an object that names its active child, {"onAWalk":"walking"}, nested as deep as the states are; an atomic state at the top is a string. matches("onAWalk") holds for every child of onAWalk. Sharing a transition, or an entry or exit action, through the parent is what keeps a large machine small. They are taken in depth in Parent states.

Parallel states

A parallel state has several children, its regions, all active at once. Each region has its own states, its own initial state and its own transitions, and moves independently of the others: the dog walks or runs, and, at the same time, wags its tail or does not. Without parallel states, each combination would need a state of its own.

const boost::json::value config = boost::json::parse(R"({
    "id": "onAWalk",
    "type": "parallel",
    "states": {
        "activity": {
            "initial": "walking",
            "states": {
                "walking": {"on": {"speed up": "running", "sees a squirrel": "running"}},
                "running": {"on": {"slow down": "walking"}}
            }
        },
        "tail": {
            "initial": "notWagging",
            "states": {
                "notWagging": {"on": {"sees a squirrel": "wagging"}},
                "wagging": {"on": {"calms down": "notWagging"}}
            }
        }
    }
})");
const xstate::result<xstate::machine> walk = xstate::create_machine(config, {});
if (!walk.has_value()) {
    return 1;
}

xstate::snapshot now = xstate::get_initial_snapshot(*walk);
std::cout << "initial: " << boost::json::serialize(now.value) << '\n';
for (const std::string_view type : {"sees a squirrel", "slow down"}) {
    const xstate::event happened{.type = std::string(type), .payload = {}};
    now = xstate::get_next_snapshot(*walk, now, happened);
    std::cout << type << " -> " << boost::json::serialize(now.value) << '\n';
}

It prints:

initial: {"activity":"walking","tail":"notWagging"}
sees a squirrel -> {"activity":"running","tail":"wagging"}
slow down -> {"activity":"walking","tail":"wagging"}

The machine is parallel at its root, "type": "parallel", and its state value names the active child of each region. One event can move several regions in the same step: sees a squirrel makes the dog run and wag at once. slow down concerns activity only, and tail stays where it is. They are taken in depth in Parallel states.

History states

A history state remembers which child of its parent was active when the parent was left. A transition that targets it goes back to that child, where a transition that targets the parent enters the parent’s initial child, so a process that is interrupted can resume where it was:

const boost::json::value config = boost::json::parse(R"({
    "id": "walk",
    "initial": "onAWalk",
    "states": {
        "onAWalk": {
            "initial": "walking",
            "states": {
                "walking": {"on": {"speed up": "running"}},
                "running": {"on": {"slow down": "walking"}},
                "previous": {"type": "history"}
            },
            "on": {"smells something": "sniffing"}
        },
        "sniffing": {"on": {"carry on": "onAWalk.previous", "start over": "onAWalk"}}
    }
})");
const xstate::result<xstate::machine> walk = xstate::create_machine(config, {});
if (!walk.has_value()) {
    return 1;
}

xstate::snapshot now = xstate::get_initial_snapshot(*walk);
for (const std::string_view type :
     {"speed up", "smells something", "carry on", "smells something", "start over"}) {
    const xstate::event happened{.type = std::string(type), .payload = {}};
    now = xstate::get_next_snapshot(*walk, now, happened);
    std::cout << type << " -> " << boost::json::serialize(now.value) << '\n';
}

It prints:

speed up -> {"onAWalk":"running"}
smells something -> "sniffing"
carry on -> {"onAWalk":"running"}
smells something -> "sniffing"
start over -> {"onAWalk":"walking"}

The dog was running when it smelled something, so carry on, which targets onAWalk.previous, takes it back to running; start over targets onAWalk itself and enters its initial child, walking. A history state is never active: it only redirects the transitions that target it. What each history state remembers is part of the snapshot.

A shallow history, the default, remembers the parent’s active child; a deep one, "history": "deep", every active state below the parent. Until the parent has been left once, the history state has nothing to remember, and the transition enters its default: the history state’s own target, or else the parent’s initial child. History states are taken in depth in History states.

Final states

A final state is where a process ends. When a machine enters a final state at its top level, it is done: its status becomes done, and it produces its output, the output of its config. A machine that is done has finished its work, and whoever runs it can stop sending it events and read the result:

const boost::json::value config = boost::json::parse(R"({
    "id": "walk",
    "initial": "waiting",
    "states": {
        "waiting": {"on": {"leave home": "onAWalk"}},
        "onAWalk": {"on": {"arrive home": "walkComplete"}},
        "walkComplete": {"type": "final"}
    },
    "output": {"walked": true}
})");
const xstate::result<xstate::machine> walk = xstate::create_machine(config, {});
if (!walk.has_value()) {
    return 1;
}

xstate::snapshot now = xstate::get_initial_snapshot(*walk);
for (const std::string_view type : {"leave home", "arrive home"}) {
    const xstate::event happened{.type = std::string(type), .payload = {}};
    now = xstate::get_next_snapshot(*walk, now, happened);
    std::cout << type << " -> " << boost::json::serialize(now.value) << ", "
              << status_name(now.status) << '\n';
}
if (now.output.has_value()) {
    std::cout << "output: " << boost::json::serialize(*now.output) << '\n';
}

It prints:

leave home -> "onAWalk", active
arrive home -> "walkComplete", done
output: {"walked":true}

status_name, a function of the program, spells the status. A snapshot’s status is active while the machine runs, done once it has reached a final state at its top level, and error once it has failed, for example when an implementation returned an error or a child’s error found no transition (Failures are values). The output here is a fixed JSON value; one computed from the context and the event is registered by name, the root’s id, in implementations.outputs (A final state’s output).

A final state inside a parent state completes that parent instead: the machine raises the event xstate.done.state.<id>, <id> being the parent’s, which the parent’s onDone transition takes. The parent stays active until a transition leaves it. A parallel state is complete once every region is in a final state. Final states are taken in depth in A parent state is done, A parallel state is done and When the machine is done.

The snapshot

A snapshot is where a machine is at one moment, with everything needed to compute what it does next. In xstate it is a plain value, xstate::snapshot, which the program keeps, copies or stores, and hands back with the next event. The machine never changes; each event yields a new snapshot.

Member What it holds

value

the state value, a string or an object of the active states

context

the context

status

active, done or error

output

the output, once the machine is done

error, error_value

why the machine failed

tags

the tags of the active states, each once

nodes

the active state nodes, by their place in the machine, in XState’s order

history

what each history state remembers

children

the child actors, each as its id and its src

The dog leaves home, and the program reads the snapshot it is in:

const boost::json::value config = boost::json::parse(R"({
    "id": "walk",
    "initial": "waiting",
    "context": {"dog": "Rex"},
    "states": {
        "waiting": {"on": {"leave home": "onAWalk"}},
        "onAWalk": {
            "tags": ["outside"],
            "initial": "walking",
            "states": {
                "walking": {"on": {"speed up": "running"}},
                "running": {}
            },
            "on": {"arrive home": "waiting"}
        }
    }
})");
const xstate::result<xstate::machine> walk = xstate::create_machine(config, {});
if (!walk.has_value()) {
    return 1;
}

const xstate::snapshot waiting = xstate::get_initial_snapshot(*walk);
const xstate::event leave_home{.type = "leave home", .payload = {}};
const xstate::snapshot out = xstate::get_next_snapshot(*walk, waiting, leave_home);
const xstate::result<bool> can_arrive =
    xstate::can(*walk, out, xstate::event{.type = "arrive home", .payload = {}});
const xstate::result<bool> can_leave = xstate::can(*walk, out, leave_home);
if (!can_arrive.has_value() || !can_leave.has_value()) {
    return 1;
}

std::cout << "value: " << boost::json::serialize(out.value) << '\n';
std::cout << "context: " << boost::json::serialize(out.context) << '\n';
std::cout << "status: " << status_name(out.status) << '\n';
const boost::json::array tags(out.tags.begin(), out.tags.end());
std::cout << "tags: " << boost::json::serialize(tags) << '\n';
std::cout << std::boolalpha << "matches onAWalk: " << out.matches("onAWalk") << '\n';
std::cout << "has tag outside: " << out.has_tag("outside") << '\n';
std::cout << "can arrive home: " << *can_arrive << '\n';
std::cout << "can leave home: " << *can_leave << '\n';

It prints:

value: {"onAWalk":"walking"}
context: {"dog":"Rex"}
status: active
tags: ["outside"]
matches onAWalk: true
has tag outside: true
can arrive home: true
can leave home: false

A tag names a group of states, so code can ask about the group, with has_tag, without naming each state in it (Tags). can asks whether an event would take a transition that does something, one with a target or an action, before sending it (Whether an event would do anything); it returns a result, since a guard it evaluates may fail.

A snapshot names its active states by their place in the machine, so it is read with the machine that computed it. The order of nodes is not cosmetic: it decides which eventless transition a parallel state takes first. To persist a snapshot, keep its value and context, and rebuild it with resolve_state, which restores those two, and from the value the active states and their tags, but nothing else: the status comes from the value, done or active, never error, and the output, the error, what history states remember and the children are not restored (Persisted state). XState’s snapshot has one more status, stopped; in xstate, whether an actor was stopped is the actor’s status, kept apart from its snapshot (Actors). States takes each member in turn (The snapshot).

Machines

A state machine describes how something behaves: the states it can be in, and the event that takes it from one state to another. A machine is the logic of an actor, not the actor itself. It holds no current state: the state it is in lives in a snapshot, and a running instance of it is an actor. Any number of snapshots and actors are made from one machine, each independent of the others.

A machine is built from two things: a config, the description XState’s createMachine(config) takes, and implementations, the code the config names. xstate reads the config as JSON and takes the implementations by name. This section shows how a machine is created, what its config holds, how implementations are registered and why by name, why a machine never changes, what creation refuses, how to find a state node, and how a machine is written out as XState’s definition and loaded back.

Creating a machine

create_machine(config, implementations) builds a machine from a config, a boost::json::value, and the implementations the config names. It returns a result<machine>: the machine, or the error that refused the config (What creation refuses). Stately’s feedback machine has two states, question and thanks, and goes from the first to the second on the event feedback.good:

const boost::json::value config = boost::json::parse(R"({
    "id": "feedback",
    "initial": "question",
    "states": {
        "question": {
            "on": {
                "feedback.good": {"target": "thanks"}
            }
        },
        "thanks": {}
    }
})");
const xstate::result<xstate::machine> feedback = xstate::create_machine(config, {});
if (!feedback.has_value()) {
    return 1;
}

const xstate::snapshot question = xstate::initial_transition(*feedback).first;
std::cout << boost::json::serialize(question.value) << '\n';

const xstate::event good{.type = "feedback.good", .payload = {}};
const xstate::snapshot thanks = xstate::transition(*feedback, question, good).first;
std::cout << boost::json::serialize(thanks.value) << '\n';

It prints:

"question"
"thanks"

Boost.JSON parses the config from a string; a program may as well read it from a file or a request. It names no implementation, so the second argument is empty, {}. Once has_value() says the result holds a machine, *feedback is that machine. initial_transition and transition are XState’s pure functions initialTransition and transition: each returns, as a pair, the snapshot the machine reaches and the actions its caller executes, and changes nothing else, so .first is the snapshot. Pure transitions and the macrostep cursor has all of them and the cursor they are built on. Stately’s page runs the machine as an actor, with createActor; in xstate an actor_system does that, as Actors shows.

The config

A config describes the machine: its states, the transitions between them, the actions they run, its context. In xstate it is the object XState’s createMachine takes, written as JSON. JSON holds data only, where XState’s config, a JavaScript object, may also hold functions: an inline action, a guard written as an arrow function, a context computed from the input. That is a choice, for three reasons:

  • A config is data a person writes, or exports from Stately Studio, and a program loads at run time, from a file, a database or a request, with nothing to compile.

  • The JSON is XState’s config: xstate’s tests run their configs on XState itself and compare the two, one microstep at a time (Testing). Only XState’s higher-order guards, functions there, take a JSON form of xstate’s (Higher-order guards and stateIn).

  • C++ has no object literal that holds functions. A config holding C++ code would be C++ code too, compiled into the program.

What a function does in XState’s config, a name does in xstate’s: the config names the function, and the implementations hold it (Implementations). An action or a guard is named as XState’s JSON writes it, "entry": "notify", or {"type": "notify", "params": {...}} with fixed params, which the implementation reads from its action_args together with the context and the event. A higher-order guard is a JSON object, {"type": "xstate.not", "guards": [...]} and the like. Appendix A maps each function XState’s config may hold to its form in xstate (Functions become implementations registered by name), and Functions become names gives the three that cannot keep what they do in XState.

The members xstate reads, on any state unless the table says otherwise:

Member What it is Section

id

The state’s id; on the root, the machine’s id

State nodes and ids

states, initial

The child states, by key, and the child a compound state starts in: its key, or {"target": "<key>", "actions": [...]}, which may also carry a meta and a description

States

type

atomic, compound, parallel, final or history; without it, a state with children is compound, one whose history is truthy as JavaScript reads it is a history state, and any other is atomic, as in XState: a history of false, null, "" or 0 leaves it atomic

States

history, target

A history state’s depth, deep for "deep" and shallow otherwise, as in XState, and its default target

States

context

On the root: the initial context. A null one, or none, is {}, as in XState; any other value that is not an object is kept as written, even false, 0 or "", which XState makes {} (A context that is not an object)

Context

on

The transitions, by event type

Events and transitions

always, after

The eventless transitions, and the delayed ones by delay

Events and transitions

onDone

The transition a compound or parallel state takes when it is done

States

entry, exit

The actions run when the state is entered and when it is left

Actions

invoke

The actors the state runs while it is active, src naming each

Actors

output

On a final state, its output; on the root, the machine’s

Input and output

tags, meta, description

Data the state carries for its readers: its tags, a string or a list of strings; its meta, any value, a null one being none; and its description, a string

States

version

On the root: the machine’s version, which its definition repeats on every node; a machine loaded from a definition has the definition’s

The definition

A transition is a target, an object (target, actions, guard, reenter, description, meta) or a list of them (Events and transitions). A target names a sibling of the transition’s state by its key, or a state below that sibling by a path of keys joined by dots, "red.stop"; a state below the transition’s own state by a path that starts with a dot, ".child"; or any state by its id, "#id" (Targets). states, on and after are read in the order JavaScript enumerates an object’s keys, as XState reads them: a key that is an array index, such as "500", first, by number, then the rest as written (State nodes and ids).

xstate reads no other member of a transition but cond, XState v4’s name for guard: a truthy cond refuses the machine, as XState’s development build refuses it (What creation refuses). types, which only TypeScript reads, and schemas, which XState keeps on the machine without using, are left unread. So is options: XState uses its maxIterations to throw on a macrostep that never settles, and in xstate the bound is the fuel of the caller that drives the cursor (Fuel). So is XState’s route, which lets the event {"type": "xstate.route", "to": "#<id>"} reach, from any state, a state that has an id and a route; in xstate that event selects no transition.

Implementations

Implementations are the code a machine runs that is not its states and transitions: actions, the side effects; guards, the conditions a transition is taken on; delays, the time before a delayed transition; and actors, what a state invokes. Three more compute a value: the initial context from the input, an invoked child’s input, and a final state’s output.

implementations holds them by name, as XState’s setup({ actions, guards, delays, actors }) registers them, so that the config refers to each with a string. In xstate a name is the only way, because a JSON config holds nothing else, and registering by name gives three things:

  • The config stays data, XState’s own, and to_json writes the machine back as XState’s definition (The definition).

  • One config runs with different implementations (A machine is immutable).

  • Creation checks the names the config uses: a guard, an after delay or an actor the implementations do not hold refuses the machine (What creation refuses), where XState fails only when it first needs the name, or never (A config is refused when it is created). A delay that an action implementation names in its delay is not checked: one that is not registered means no delay, as in XState.

An action is the exception: an action name the implementations do not hold is not an error. It is a custom action, which xstate returns to its caller to execute, as XState’s transition returns it (Custom actions). That is how a caller’s own effects, a log line, a request, a write, reach it.

Stately’s setup example registers two assign actions, increment and decrement, which the config names under on:

/** The context's count plus `step`, as an assign merges it into the context. */
xstate::result<boost::json::object> add(const xstate::action_args& args, std::int64_t step) {
    const boost::json::object* context = args.context.if_object();
    const boost::json::value* count = context == nullptr ? nullptr : context->if_contains("count");
    const std::int64_t* value = count == nullptr ? nullptr : count->if_int64();
    if (value == nullptr) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"count", *value + step}};
}

const xstate::assign_action increment{
    .assignment = [](const xstate::action_args& args) { return add(args, 1); },
};
const xstate::assign_action decrement{
    .assignment = [](const xstate::action_args& args) { return add(args, -1); },
};
xstate::implementations implementations;
implementations.actions.emplace("increment", increment);
implementations.actions.emplace("decrement", decrement);

const boost::json::value config = boost::json::parse(R"({
    "context": {"count": 0},
    "on": {
        "inc": {"actions": "increment"},
        "dec": {"actions": "decrement"}
    }
})");
const xstate::result<xstate::machine> counter = xstate::create_machine(config, implementations);
if (!counter.has_value()) {
    return 1;
}

xstate::snapshot now = xstate::initial_transition(*counter).first;
for (const std::string_view type : {"inc", "inc", "dec"}) {
    const xstate::event sent{.type = std::string(type), .payload = {}};
    now = xstate::transition(*counter, now, sent).first;
    std::cout << type << ' ' << boost::json::serialize(now.context) << '\n';
}

It prints:

inc {"count":1}
inc {"count":2}
dec {"count":1}

increment and decrement are each an assign_action, holding an assigner that returns the members to change, and emplace registers each in implementations.actions under the name the config uses. xstate merges what an assigner returns into the context as Object.assign does, and fails with implementation_failed on a context that is not an object (A context that is not an object). A context and an event are JSON values, which no compiler checks, as XState’s types let TypeScript check them; so an implementation checks what it reads before it reads it, as add does, and returns implementation_failed when the context is not what it expects.

Guards and delays are registered the same way, in implementations.guards and implementations.delays. After Stately’s example, a delayed transition is guarded by canAccess and delayed by shortDelay; its action, trackAccess, is left unregistered:

/** Whether the context's role is "admin". */
xstate::result<bool> can_access(const xstate::action_args& args) {
    const boost::json::object* context = args.context.if_object();
    const boost::json::value* role = context == nullptr ? nullptr : context->if_contains("role");
    return role != nullptr && role->is_string() && role->get_string() == "admin";
}

xstate::implementations implementations;
implementations.guards.emplace("canAccess", can_access);
implementations.delays.emplace("shortDelay", [](const xstate::action_args&) {
    return xstate::result<std::uint64_t>(250);
});

const boost::json::value config = boost::json::parse(R"({
    "id": "access",
    "context": {"role": "admin"},
    "initial": "checking",
    "states": {
        "checking": {
            "after": {
                "shortDelay": {
                    "guard": "canAccess",
                    "actions": "trackAccess",
                    "target": "allowed"
                }
            }
        },
        "allowed": {}
    }
})");
const xstate::result<xstate::machine> access = xstate::create_machine(config, implementations);
if (!access.has_value()) {
    return 1;
}

const auto [checking, entry_actions] = xstate::initial_transition(*access);
for (const xstate::action& entry : entry_actions) {
    std::cout << entry.type << ' ' << boost::json::serialize(entry.params) << '\n';
}

const xstate::event delayed{.type = "xstate.after.shortDelay.access.checking", .payload = {}};
const auto [allowed, actions] = xstate::transition(*access, checking, delayed);
std::cout << boost::json::serialize(allowed.value);
for (const xstate::action& taken : actions) {
    std::cout << ' ' << taken.type;
}
std::cout << '\n';

It prints:

xstate.raise {"event":{"type":"xstate.after.shortDelay.access.checking"},"id":"xstate.after.shortDelay.access.checking","delay":250}
"allowed" xstate.cancel trackAccess

Entering checking returns the delayed xstate.raise of its after event, whose delay, 250, shortDelay resolved. A delay is always a function, here one that ignores its arguments, where XState’s delays also takes a plain number. The example sends the after event itself, as a clock would once the delay has passed; delayed transitions in the machine core shows the simulated_clock that does it. canAccess lets the transition through, leaving checking returns the xstate.cancel of the delayed event, and trackAccess, which nothing implements, comes back as a custom action.

implementations is a value: a module that adds its own entries to another’s copies it and inserts them, where XState’s setup.extend adds implementations to an existing setup. It holds seven members:

Member Keyed by What it holds Section

actions

action name

XState’s setup({ actions }): one of xstate’s built-in actions, assign_action, raise_action, log_action, send_to_action and the others, with the functions it resolves with

Actions

guards

guard name

XState’s setup({ guards }): a predicate, which returns result<bool>

Events and transitions

delays

delay name

XState’s setup({ delays }): a delay_maker, which returns the delay in milliseconds, result<std::uint64_t>

Events and transitions

actors

src

XState’s setup({ actors }): a machine_actor, a machine run as a child, or a host_actor, work the host does, in place of fromPromise

Actors

context

(one function)

XState’s context: ({ input }) => ...: a context_maker, which returns the initial context for the input, in place of the config’s context. A value that is not an object fails the initial macrostep with implementation_failed, where XState merges it into {} (A context that is not an object)

Context

inputs

invoke id

An invoke’s input: ({ context, event }) => ...: a value_maker

Input and output

outputs

state id

A final state’s output: ({ context, event }) => ..., or, by the root’s id, the machine’s: a value_maker

Input and output

Built-in and custom actions lists the built-in actions, each with what the core returns for it. The machine core runs no actor itself: entering a state that invokes one resolves into an xstate.spawnChild action and leaving it into an xstate.stopChild, an invoke’s onDone and onError are transitions on xstate.done.actor.<id> and xstate.error.actor.<id>, and the actor layer runs the child and delivers those events (Invoking actors).

The context’s function receives the input. Every other function the implementations hold receives an action_args: the context as it is when the function runs, the event of the microstep, and the params the config gave the action or the guard, null when it gave none. The root’s output is the one exception to the event: it receives the done event that completed the machine, as XState’s does. For a compound root, that is the done event of the top-level final state, which carries that state’s output; for a parallel root, it is the root’s own done event, which carries none (The machine’s output and its final state). Each function returns a result, and an error it returns stops the macrostep, which leaves the snapshot it began from with the status error, where XState’s implementation would have thrown (Failures are values).

Three rules hold for the implementations, each checked at creation:

  • An implementation holding an empty function, a default-constructed std::function, refuses the machine with invalid_config: calling it would abort in a build without exceptions. An optional function left out holds none: a log_action without a value is a log of the context and the event, as in XState.

  • An inputs entry names an invoke whose config gives no input, and an outputs entry a final state, or the root, whose config gives no output; any other is invalid_config.

  • Every actor the config names, by an invoke’s src or through a spawn_child_action it names as an action, is an entry of actors; any other is unknown_actor. A registered spawn_child_action the config never names is not checked, as XState’s setup accepts it.

A machine is immutable

A machine never changes once it is created. The state it is in lives in a snapshot, a value of its own, so one machine serves any number of snapshots and actors at once, and nothing one of them does reaches another. A machine is a handle to data its copies share: copying one copies a pointer, not the tree, so a cursor, an actor or a machine_actor keeps a copy. The transitions a microstep reports point into the machine, and stay valid while any copy of it lives.

To run one config with other implementations, create a second machine from it. create_machine takes the whole set of implementations, so a program that changes some of them copies the first machine’s, registry(), and replaces the entries that change. XState’s machine.provide does this in one call; xstate has no provide (What is not ported).

const boost::json::value config = boost::json::parse(R"({
    "context": {"count": 0},
    "on": {
        "inc": {"actions": "increment"}
    }
})");

const xstate::assign_action plus_one{
    .assignment = [](const xstate::action_args& args) { return add(args, 1); },
};
xstate::implementations adds_one;
adds_one.actions.emplace("increment", plus_one);
const xstate::result<xstate::machine> counter = xstate::create_machine(config, adds_one);
if (!counter.has_value()) {
    return 1;
}

const xstate::assign_action plus_ten{
    .assignment = [](const xstate::action_args& args) { return add(args, 10); },
};
xstate::implementations adds_ten = counter->registry();
adds_ten.actions.insert_or_assign("increment", plus_ten);
const xstate::result<xstate::machine> by_ten = xstate::create_machine(config, adds_ten);
if (!by_ten.has_value()) {
    return 1;
}

const auto increment_once = [](const xstate::machine& machine) {
    const xstate::snapshot initial = xstate::initial_transition(machine).first;
    const xstate::event inc{.type = "inc", .payload = {}};
    return xstate::transition(machine, initial, inc).first.context;
};
std::cout << "counter " << boost::json::serialize(increment_once(*counter)) << '\n';
std::cout << "byTen " << boost::json::serialize(increment_once(*by_ten)) << '\n';

It prints:

counter {"count":1}
byTen {"count":10}

add is the function of the previous section’s example. by_ten starts from a copy of the implementations of counter, in which insert_or_assign replaces increment and keeps any other entry. Both machines are created from one parsed config; neither changes the other, nor the config.

What creation refuses

A config is checked once, when the machine is created, and a config that is not a machine gives no machine: create_machine returns the error instead. Every machine comes from create_machine, which create_machine_from_definition calls too, so a machine that exists has passed every check, and a mistake in a config shows when the config is loaded, not when an event first reaches it.

The error is a value, not an exception. xstate throws no exception of its own, and builds with exceptions turned off, which the lint checks; where XState throws, xstate returns an error. result<T> is boost::system::result<T, boost::system::error_code>; its error is a code of the category webcpp.xstate, one of the values of errc, and message() spells its name. Check has_value() before using a result.

Here a transition is guarded by isValid, which the implementations do not hold:

const boost::json::value config = boost::json::parse(R"({
    "initial": "editing",
    "states": {
        "editing": {
            "on": {
                "submit": {"guard": "isValid", "target": "submitted"}
            }
        },
        "submitted": {}
    }
})");
const xstate::result<xstate::machine> form = xstate::create_machine(config, {});
if (!form.has_value()) {
    const boost::system::error_code& error = form.error();
    std::cout << error.category().name() << ": " << error.message() << '\n';
}

It prints:

webcpp.xstate: unknown_guard

xstate finds the missing name when the machine is loaded, before any event. XState creates this machine and starts it, and throws only when submit arrives and the guard is evaluated (A config is refused when it is created). XState refuses some configs in createMachine, others only when the machine starts or an event reaches the broken part, and accepts some; xstate refuses each of those below at creation, and the last column says what XState does with it:

The config, or the implementations Error XState

Is not a JSON object, or has a member xstate cannot read, such as an id or a tag that is not a string, a type xstate does not know, or a history target that is not a string or a list of strings

invalid_config

Accepts each of these at creation: a list is a machine with no state, a non-string id or tag and an unknown type run, and a history target that is not a string throws a TypeError when the history state is entered

Has an entry, exit or actions that is null, or a tags that is null

invalid_config

Reads it, as its toArray does, as a list that holds null: the machine runs with the null tag, and fails when it runs the null action

Has a compound state without initial

invalid_config

Throws in createMachine

Has an on key of ""

invalid_config

Throws in createMachine: an eventless transition is always

Has a transition that declares a truthy cond, renamed guard in XState v5

invalid_config

Throws in createMachine, in its development build

Has a transition target that names no state

unknown_target

Throws in createMachine

Has a history state, below the root, whose target names no state

unknown_target

Throws when the history state is entered

Has an initial that names no child

unknown_target

Throws when the state is entered, the root’s when the machine starts

Has a higher-order guard without a guards list, an xstate.not with other than one operand, or an xstate.stateIn without stateValue

invalid_config

No counterpart: these are functions in XState

Names a guard the implementations do not hold

unknown_guard

Throws when the guard is evaluated

Has an after key that is not digits alone but starts with a digit, a sign or a dot, such as 1.5, -5 or 1e3

invalid_config

Reads a key JavaScript takes as a number as that number, a delay of 1.5, -5 or 1000 milliseconds, and any other as a delay’s name

Names, as an after key, a delay the implementations do not hold

unknown_delay

Takes the delayed transition at once, as if it had no delay

Invokes an actor the implementations do not hold, or spawns one through an action it names

unknown_actor

Creates no child, and fails where something reads it

Has a state whose default entry leads back to it: a history state whose default target is an ancestor whose initial states lead back to the history

invalid_config

Overflows its stack when it enters the state

Has an invoke whose src is not a string

invalid_config

Takes inline actor logic, which JSON cannot hold

Implementations: one holds an empty function

invalid_config

No counterpart

Implementations: an inputs entry names no invoke, an outputs entry names neither a final state nor the root, or either names one whose config already gives its input or output

invalid_config

No counterpart

Some members hold any value, as in XState: a state’s meta, the root’s version, the output of a final state or of the root, and a transition’s meta and description. Two values are dropped, where XState keeps them: a state’s meta that is null, which xstate reads as no meta, and a state’s description that is not a string, an exception to the first row.

One config is accepted as XState accepts it: a machine whose root is itself a history state, which has no parent to resolve its default target against. A microstep that needs that default fails with unknown_target, where XState throws (History states).

What creation cannot see fails later, and as a value too: an implementation that returns an error fails the microstep that calls it, and the snapshot it leaves has the status error (Actions).

State nodes and ids

A machine is a tree of state nodes, one for the root and one for each state of its config, and a node holds everything the config says of its state. A program reads the nodes to show a machine, to name a state in a target or a query, or to read what a snapshot’s active states are.

xstate keeps the nodes in one list, in document order: the root first, a parent before its children, and the children in the order JavaScript enumerates the keys of states, as XState reads them. A key that is an array index, such as "0" or "2", comes first, by number, and the others follow as the config writes them: the children b, 10 and 2 are listed 2, 10, b. A node is named by its index in that list, so the tree holds no pointer: size() is the number of nodes, node(index) one of them, for an index below size(), and root() is node(0).

A state_node holds what XState’s StateNode holds once it is initialised: its key in its parent’s states, its id, its path of keys from the root, its type, the index of its parent, its children, its entry and exit actions, its transitions and invokes, its tags, meta, output and description. A state’s id is the id its config gives, or, without one, the machine’s id and the state’s path, joined by dots; the machine’s id, id(), is the root’s id, (machine) when the config gives none, as in XState. An explicit id names its own node only: below, the children of playing, whose id is active, are player.playing.normal and player.playing.fast.

std::string_view type_name(xstate::node_type type) {
    switch (type) {
        case xstate::node_type::atomic: return "atomic";
        case xstate::node_type::compound: return "compound";
        case xstate::node_type::parallel: return "parallel";
        case xstate::node_type::final: return "final";
        case xstate::node_type::history: return "history";
    }
    return "unknown";
}

const boost::json::value config = boost::json::parse(R"({
    "id": "player",
    "initial": "stopped",
    "states": {
        "stopped": {"on": {"play": "playing"}},
        "playing": {
            "id": "active",
            "initial": "normal",
            "states": {"normal": {}, "fast": {}},
            "on": {"stop": "stopped"}
        },
        "done": {"type": "final"}
    }
})");
const xstate::result<xstate::machine> player = xstate::create_machine(config, {});
if (!player.has_value()) {
    return 1;
}

for (std::size_t index = 0; index < player->size(); ++index) {
    const xstate::state_node& node = player->node(index);
    std::cout << index << ' ' << node.key << ' ' << node.id << ' ' << type_name(node.type)
              << '\n';
}

const xstate::result<std::size_t> active = player->node_by_id("active");
if (!active.has_value()) {
    return 1;
}
const xstate::result<std::size_t> fast = player->child(*active, "fast");
if (!fast.has_value()) {
    return 1;
}
const xstate::result<std::size_t> found = player->node_by_id("#active.fast");
if (!found.has_value()) {
    return 1;
}
std::cout << player->node(*fast).id << ' ' << player->node(*found).id << '\n';

It prints:

0 player player compound
1 stopped player.stopped atomic
2 playing active compound
3 normal player.playing.normal atomic
4 fast player.playing.fast atomic
5 done player.done final
player.playing.fast player.playing.fast

Each line gives a node’s index, key, id and type, and type_name spells the type. The index is the node’s order in XState, as long as no two nodes of the machine share an id. Three functions find a node, each returning its index as a result<std::size_t>:

  • node_by_id(id) is XState’s getStateNodeById. The # is optional, and a path after the id walks down from that node: #active.fast is the child fast of the node whose id is active.

  • child(node, key) is the child of node whose key is key, a dot being part of the key.

  • node_by_path(from, path) is XState’s getStateNodeByPath: a path of keys relative to from, or an id when it starts with #.

A backslash escapes a dot in an id or a path: #foo\.bar names the node whose id is foo.bar, where #foo.bar names the child bar of the node foo. A name that finds no node is the error unknown_state, where XState’s getStateNodeById throws.

The machine also keeps what it was created from: config() is the config, registry() the implementations, and context() the config’s context, an empty object when the config gives none or null. The reference lists every member of machine (webcpp::xstate::machine).

The definition

A machine’s definition is the machine written out as JSON, once its config has been read: every node with its id, key, type, initial transition and children, its transitions with their targets resolved to lists of #id, its entry and exit actions, its order (the root’s is written -1), its invokes and its tags. It is what a program stores, sends or hands to a tool when it needs the machine as data, every target already resolved. XState writes it with machine.toJSON(), which JSON.stringify(machine) calls, and to_json(machine) writes the same definition:

const boost::json::value config = boost::json::parse(R"({
    "id": "toggle",
    "initial": "inactive",
    "states": {
        "inactive": {"on": {"toggle": "active"}},
        "active": {"on": {"toggle": "inactive"}}
    }
})");
const xstate::result<xstate::machine> toggle = xstate::create_machine(config, {});
if (!toggle.has_value()) {
    return 1;
}

const boost::json::value definition = xstate::to_json(*toggle);
for (const std::string_view pointer : {
         "/id",
         "/order",
         "/initial/target",
         "/states/inactive/type",
         "/states/inactive/order",
         "/states/inactive/on/toggle/0/target",
         "/states/inactive/on/toggle/0/source",
     }) {
    boost::system::error_code missing;
    const boost::json::value* found = definition.find_pointer(pointer, missing);
    if (found == nullptr) {
        return 1;
    }
    std::cout << pointer << ' ' << boost::json::serialize(*found) << '\n';
}

It prints:

/id "toggle"
/order -1
/initial/target ["#toggle.inactive"]
/states/inactive/type "atomic"
/states/inactive/order 1
/states/inactive/on/toggle/0/target ["#toggle.active"]
/states/inactive/on/toggle/0/source "#toggle.inactive"

The program reads the definition with JSON pointers: the root’s order is -1, a state’s own type is written even where its config leaves it out, and a transition’s target and source are ids. to_json writes XState’s definition key for key, with two exceptions. It leaves out the two values creation drops, a state’s meta that is null and a description that is not a string, which XState writes (What creation refuses). And it adds the higher-order guards, which XState cannot write: they are functions in XState, and JSON.stringify leaves a function out. The members of an object may come in another order than XState’s, so compare two definitions as JSON values, with ==, and not as text.

create_machine_from_definition(definition, implementations, context) loads a definition back, one to_json wrote or one XState wrote. A definition holds names, not code, so the implementations are given again; and it holds no context, so the context is given too. It returns a result<machine>. Here a toggle is written out, stored as text, and loaded back:

const boost::json::value config = boost::json::parse(R"({
    "id": "toggle",
    "version": "1.0",
    "initial": "inactive",
    "context": {"presses": 0},
    "states": {
        "inactive": {"on": {"toggle": "active"}},
        "active": {"on": {"toggle": "inactive"}}
    }
})");
const xstate::result<xstate::machine> toggle = xstate::create_machine(config, {});
if (!toggle.has_value()) {
    return 1;
}
const std::string stored = boost::json::serialize(xstate::to_json(*toggle));

boost::system::error_code malformed;
const boost::json::value definition = boost::json::parse(stored, malformed);
if (malformed) {
    return 1;
}
const xstate::result<xstate::machine> reloaded = xstate::create_machine_from_definition(
    definition, xstate::implementations(), toggle->context());
if (!reloaded.has_value()) {
    return 1;
}

const xstate::snapshot initial = xstate::initial_transition(*reloaded).first;
const xstate::event pressed{.type = "toggle", .payload = {}};
const xstate::snapshot next = xstate::transition(*reloaded, initial, pressed).first;
std::cout << boost::json::serialize(next.value) << ' ' << boost::json::serialize(next.context)
          << '\n';
std::cout << std::boolalpha << (xstate::to_json(*reloaded) == definition) << '\n';

It prints:

"active" {"presses":0}
true

The reloaded machine takes the original’s step, starts from the context it was given, and writes the definition it was loaded from, the toggle’s version on every node included. XState has no function that creates a machine from a definition.

The loader refuses with invalid_config what XState never writes: a value that is not an object, and a state whose states, on, invoke, entry or exit does not have the JSON type a definition gives it, an object of state objects, an object of lists of transition objects, a list of invoke objects, a list of actions; and, in a state that has states, an initial whose first target is not a string. From any other object it rebuilds the config the definition describes and creates the machine with create_machine, which checks it as it checks any config (What creation refuses): an id that is not a string, or a type xstate does not know, is invalid_config there. So is an entry, exit, actions or tags list that holds null, the list XState writes for one of them that is null, which creation refuses in a config too. A compound state’s initial is rebuilt from its first target, # and the id of a child, with the actions, meta and description of the written initial transition; a target that names none of the children leaves the state without an initial, which is invalid_config too, where a config’s initial that names no child is unknown_target.

Three members repeat what the rest of the definition says and are not read, whatever they hold: key, order and transitions; nor is the version of a state below the root, which repeats the root’s. The root’s version becomes the machine’s, which to_json writes on every node again.

A definition does not hold all of a config either. The definition, XState’s and so the one to_json writes, leaves out the context, the eventless transitions (always) and a history state’s default target; two states may share an id when a key holds a dot; and an entry or exit action named xstate.raise or xstate.cancel, a custom action, cannot be told from the raise and cancel an after adds, which the definition writes under those names, so the loader drops it with them. Given the original’s context, a machine loaded from a definition takes the original’s steps and writes the same definition whenever the original has none of those; one loaded from a definition XState wrote writes it without the two values to_json leaves out. A definition XState wrote has no higher-order guard either: the transition comes back unguarded, and the reloaded machine takes it whatever the guard would have decided. One to_json wrote keeps the guard.

The sections that follow take the config apart: States, Context, Events and transitions, Actions and Input and output.

States

A state is a mode of a machine: what it is doing now, and so which events it reacts to and how. A video player is paused or playing; a feedback form is prompting the user, showing a form, thanking them or closed. A machine is in one of these states at a time, out of a set fixed when the machine is written, which is why they are called finite (a parallel state counts as one, made of its regions). It moves between them only through the transitions its config declares, so an impossible state, a form being filled in and submitted at the same time, cannot happen.

States nest. A state with child states is a parent state, a state whose children are all active at once is a parallel state, and the machine itself is the root of that tree. The active states and the context, the machine’s data (Context), make up the machine’s whole condition, which xstate, as XState, calls a snapshot.

This section covers the snapshot and how to read it, then each kind of state: initial, parent, parallel, final and history states, and the tags, meta data and descriptions a state carries.

The examples step their machines with the core’s pure functions (Pure transitions and the macrostep cursor). An example that prints only snapshots calls get_initial_snapshot and get_next_snapshot, which return the snapshot alone; one that also prints the actions a step returns calls initial_transition and transition, which return both, as XState’s initialTransition and transition do. XState 5.33.2 deprecates its getInitialSnapshot and getNextSnapshot in favour of those two; xstate keeps both pairs. An actor hands out the same snapshots, through snapshot_of in xstate’s actor layer (Snapshots and status). Each program is in example/xstate/, named after its topic: states_finite.cpp, states_snapshot.cpp and so on.

Finite states

A config lists the machine’s states under states, each by its key, and names the one the machine starts in with initial. The snapshot’s value says which state is active, here by its key.

const boost::json::value config = boost::json::parse(R"({
    "id": "feedback",
    "context": {"name": "", "email": "", "feedback": ""},
    "initial": "prompt",
    "states": {
        "prompt": {"on": {"feedback.good": "thanks", "feedback.bad": "form"}},
        "form": {"on": {"feedback.submit": "thanks"}},
        "thanks": {"on": {"feedback.close": "closed"}},
        "closed": {"type": "final"}
    }
})");
const xstate::result<xstate::machine> feedback = xstate::create_machine(config, {});
if (!feedback.has_value()) {
    return 1;
}

const xstate::snapshot first = xstate::get_initial_snapshot(*feedback);
std::cout << boost::json::serialize(first.value) << '\n';  // the finite state
std::cout << boost::json::serialize(first.context) << '\n';

const xstate::event good{.type = "feedback.good", .payload = {}};
const xstate::snapshot next = xstate::get_next_snapshot(*feedback, first, good);
std::cout << boost::json::serialize(next.value) << '\n';

It prints:

"prompt"
{"name":"","email":"","feedback":""}
"thanks"

The snapshot holds both halves of the machine’s condition: value, the finite state, and context, the data no finite set of states could enumerate, such as what the user typed (Context). The event feedback.good moved the machine from prompt to thanks; the context did not change, as no action assigned to it.

Each key under states is a state node, and the machine itself is the root node. A node has an id: the one its config gives with id, or else the machine’s id and the node’s path joined by dots, feedback.prompt here. A machine without an id is (machine), as in XState. machine::node_by_id finds a node by its id and machine::node reads it (State nodes and ids). An id does not change the state value; it is another way for a transition to name the state (Targets).

The snapshot

A snapshot is where a machine is at one moment: its state value, its context, whether it still runs, and what it produced. xstate’s snapshot is a plain struct: XState’s snapshot without its machine, its node objects, its children’s actor objects and the methods that read them. You keep it, copy it, store it, and hand it back to the function that computes the next one.

It holds no pointer into its machine: it names the active state nodes by their index in the machine. A value that points nowhere has no lifetime to manage, so it can wait between two requests or two microsteps, and a copy is independent of the original. A query that needs the tree of nodes takes the machine as an argument instead, as every pure function does (get_meta, can), where XState’s snapshot carries its machine and reads it through its methods.

Two rules follow from this. A snapshot’s nodes are indexes into the machine that computed it, so a snapshot is read and advanced only with that machine, or with one created from the same config. And its members are public so they can be read: the next step reads both value, to select an event’s transitions, and nodes, to select eventless ones and to exit and enter states, so a snapshot edited by hand is not one xstate computed. To put a machine in a given state, build the snapshot with resolve_state (Persisted state).

Member What it holds XState

value

The state value: which states are active (The state value). xstate computes it from nodes, so the two never disagree.

value

context

The context, a boost::json::value (Context).

context

status

status::active while the machine runs, status::done once it has reached a top-level final state, or every region of a parallel root has (When the machine is done), status::error once a step has failed.

status, which has a fourth value, 'stopped'

output

The machine’s output once it is done, when it has one; none otherwise, where XState’s is undefined (Output).

output

error

Why the machine failed, when status is error: the code a failing implementation returned, or one of xstate’s errc (webcpp::xstate::errc). Empty otherwise.

error, the value an implementation threw

error_value

When error is actor_failed, the failed child’s error, if its error event carried one (Done and error events).

error

nodes

The active state nodes, root included, by their index in the machine, in XState’s order.

_nodes, internal

history

What each history state remembers, by the history node’s id (History states).

historyValue

tags

The tags of the active nodes, each once, in the order of nodes (Tags).

tags, a Set

children

The machine’s child actors, as (id, src) pairs, in the order JavaScript enumerates XState’s children object. The core only records them: its caller runs them, from the xstate.spawnChild and xstate.stopChild actions a step returns (Actors).

children, each child’s ActorRef by its id

XState’s 'stopped' is the status of a stopped actor, and the status its transition gives a snapshot that receives the event xstate.stop. In xstate the actor layer reports a stopped actor through status_of (Snapshots and status), and the core runs xstate.stop as any other event (Events).

Its queries are matches (Matching a state value), has_tag (Tags) and remembered (History states); get_meta(machine, snapshot) (Meta and descriptions) and can(machine, snapshot, event) (Whether an event would do anything) take the machine. A snapshot is rebuilt from a state value and a context with resolve_state (Persisted state); XState’s toJSON of a snapshot has no counterpart.

XState’s mapState(snapshot, mapper), which Stately’s page on states shows, walks the active nodes from each state without children up to the root and calls, for each one, the map function the mapper object gives for its path. xstate has no mapState: a program reads snapshot.nodes and maps each node by its id or its path, machine.node(index), as describe does in the example below.

The example prints the members of a snapshot before and after an event that completes the machine:

std::string_view name_of(xstate::status status) {
    switch (status) {
        case xstate::status::active: return "active";
        case xstate::status::done: return "done";
        case xstate::status::error: return "error";
    }
    return "unknown";
}

/** Prints what a snapshot holds, naming its active nodes by their ids. */
void describe(const xstate::machine& owner, const xstate::snapshot& now) {
    boost::json::array nodes;
    for (const std::size_t node : now.nodes) {
        nodes.emplace_back(owner.node(node).id);
    }
    const boost::json::array tags(now.tags.begin(), now.tags.end());
    std::cout << "value: " << boost::json::serialize(now.value) << '\n';
    std::cout << "context: " << boost::json::serialize(now.context) << '\n';
    std::cout << "status: " << name_of(now.status) << '\n';
    std::cout << "nodes: " << boost::json::serialize(nodes) << '\n';
    std::cout << "tags: " << boost::json::serialize(tags) << '\n';
    if (now.status == xstate::status::done && now.output.has_value()) {
        std::cout << "output: " << boost::json::serialize(*now.output) << '\n';
    }
}

const boost::json::value config = boost::json::parse(R"({
    "id": "feedback",
    "context": {"feedback": ""},
    "initial": "form",
    "states": {
        "form": {
            "tags": ["visible"],
            "initial": "invalid",
            "states": {
                "invalid": {"on": {"feedback.valid": "valid"}},
                "valid": {}
            },
            "on": {"feedback.submit": "thanks"}
        },
        "thanks": {"type": "final"}
    },
    "output": {"sent": true}
})");
const xstate::result<xstate::machine> feedback = xstate::create_machine(config, {});
if (!feedback.has_value()) {
    return 1;
}

const xstate::snapshot first = xstate::get_initial_snapshot(*feedback);
describe(*feedback, first);

const xstate::event submit{.type = "feedback.submit", .payload = {}};
describe(*feedback, xstate::get_next_snapshot(*feedback, first, submit));

It prints:

value: {"form":"invalid"}
context: {"feedback":""}
status: active
nodes: ["feedback","feedback.form","feedback.form.invalid"]
tags: ["visible"]
value: "thanks"
context: {"feedback":""}
status: done
nodes: ["feedback.thanks","feedback"]
tags: []
output: {"sent":true}

The first snapshot is in form, and in its initial child invalid; nodes lists every active node, from the root down, and tags the one tag form carries. feedback.submit takes the machine to thanks, a final child of the root, so the machine is done: its status is done and its output is the config’s output.

The order of nodes is XState’s, and xstate keeps it because it is not cosmetic: it decides the order in which the regions of a parallel state take their eventless transitions. The nodes a microstep keeps keep their place, those it exits leave, and those it enters are appended in document order; a microstep that ends with the same active nodes, and in which no history state records anything, keeps the order it had. Once a step completes the machine, XState sorts the nodes in reverse document order, a child before its parent, and so does xstate: feedback.thanks before feedback.

Document order is the order of the states in the config as XState reads it: a parent before its children, and siblings in the order JavaScript enumerates the keys of states, a key that is an array index, such as 0 or 10, first, by number, then the rest as written. A parallel state whose config writes its regions b, 10, 2 and a enters them in the order 2, 10, b, a.

The state value

The state value names the active states in JSON, the same JSON XState writes, its keys in the same order:

  • the key of the active child, a string, when that child has no children of its own;

  • an object when it has: the child’s key, and under it the child’s own value (Parent states);

  • an object with one member per region for a parallel state (Parallel states);

  • {} for a machine that has no states but its root.

const boost::json::value config = boost::json::parse(R"({
    "id": "feedback",
    "initial": "question",
    "states": {
        "question": {"on": {"next": "form"}},
        "form": {
            "initial": "invalid",
            "states": {"invalid": {}, "valid": {}},
            "on": {"settings": "settings"}
        },
        "settings": {
            "type": "parallel",
            "states": {
                "display": {"initial": "bright", "states": {"bright": {}, "dim": {}}},
                "theme": {"initial": "dark", "states": {"dark": {}, "light": {}}}
            }
        }
    }
})");
const xstate::result<xstate::machine> feedback = xstate::create_machine(config, {});
if (!feedback.has_value()) {
    return 1;
}

xstate::snapshot now = xstate::get_initial_snapshot(*feedback);
std::cout << boost::json::serialize(now.value) << '\n';

const xstate::event next{.type = "next", .payload = {}};
now = xstate::get_next_snapshot(*feedback, now, next);
std::cout << boost::json::serialize(now.value) << '\n';

const xstate::event settings{.type = "settings", .payload = {}};
now = xstate::get_next_snapshot(*feedback, now, settings);
std::cout << boost::json::serialize(now.value) << '\n';

const boost::json::value counting_config = boost::json::parse(R"({"id": "counting"})");
const xstate::result<xstate::machine> counting = xstate::create_machine(counting_config, {});
if (!counting.has_value()) {
    return 1;
}
std::cout << boost::json::serialize(xstate::get_initial_snapshot(*counting).value) << '\n';

It prints:

"question"
{"form":"invalid"}
{"settings":{"display":"bright","theme":"dark"}}
{}

An object value writes its keys in the order JavaScript enumerates an object’s keys, as XState’s value is a JavaScript object: a key that is an array index first, by number, then the rest in the order of nodes. The parallel state whose config writes its regions b, 10, 2 and a (The snapshot) writes them 2, 10, b, a in its value when it is entered, and 2, 10, a, b when it is the root and a step completes the machine, as that step sorts nodes in reverse document order. The regions of a parallel state take an event’s transitions in the order of its value’s keys, and their eventless transitions in the order of nodes.

Stately’s page on states says the value of a machine without states is null; XState 5.33.2 writes {}, and so does xstate.

Matching a state value

snapshot.matches(value) says whether the machine is in a state: whether the value given is the snapshot’s value or a part of it, as XState’s snapshot.matches does. A string names a state by its key, or a nested one by its path with dots, "form.invalid"; an object names nested and parallel states, and needs to list only the ones it tests. matches_state(parent, child) says whether the state value child is parent or a state within it, XState’s matchesState; snapshot.matches(v) is matches_state(v, snapshot.value).

const boost::json::value config = boost::json::parse(R"({
    "id": "feedback",
    "initial": "question",
    "states": {
        "question": {"on": {"feedback.bad": "form"}},
        "form": {"initial": "invalid", "states": {"invalid": {}, "valid": {}}}
    }
})");
const xstate::result<xstate::machine> feedback = xstate::create_machine(config, {});
if (!feedback.has_value()) {
    return 1;
}
const xstate::event bad{.type = "feedback.bad", .payload = {}};
const xstate::snapshot now =
    xstate::get_next_snapshot(*feedback, xstate::get_initial_snapshot(*feedback), bad);
std::cout << "value: " << boost::json::serialize(now.value) << '\n';

const boost::json::value tested = boost::json::parse(R"([
    "question",
    "form",
    {"form": "invalid"},
    {"form": "valid"},
    "form.invalid"
])");
std::cout << std::boolalpha;
for (const boost::json::value& value : tested.get_array()) {
    std::cout << boost::json::serialize(value) << ": " << now.matches(value) << '\n';
}

const boost::json::value playing =
    boost::json::parse(R"({"track": "playing", "volume": "normal"})");
const boost::json::value track_playing = boost::json::parse(R"({"track": "playing"})");
std::cout << boost::json::serialize(track_playing) << " in " << boost::json::serialize(playing)
          << ": " << xstate::matches_state(track_playing, playing) << '\n';

It prints:

value: {"form":"invalid"}
"question": false
"form": true
{"form":"invalid"}: true
{"form":"valid"}: false
"form.invalid": true
{"track":"playing"} in {"track":"playing","volume":"normal"}: true

"form" matches because the machine is in form, whichever child of form is active; {"form": "valid"} does not, because the active child is another. The last line tests a part of the value of a parallel state: {"track": "playing"} is within {"track": "playing", "volume": "normal"}. The order of the arguments matters, the value tested first: the same two values the other way round do not match. When the question is what the machine is doing rather than where it is, a tag survives a reorganisation of the states better than a state value (Tags).

Initial states

A machine starts in its initial state, the child of the root that initial names. A parent state names one too: entering the parent enters its initial child, and, when that child is a parent, the child’s own initial child, and so on until every active branch reaches a state without children. A machine has at most one initial state, so it always knows where to start. A parallel state, the root included, has none: entering it enters every region (Parallel states).

/** The types of a list of actions, as a JSON array. */
boost::json::array types_of(const std::vector<xstate::action>& actions) {
    boost::json::array types;
    for (const xstate::action& one : actions) {
        types.emplace_back(one.type);
    }
    return types;
}

const boost::json::value config = boost::json::parse(R"({
    "id": "light",
    "initial": "green",
    "states": {
        "green": {"on": {"timer": "yellow"}},
        "yellow": {"on": {"timer": "red", "emergency": "red.stop"}},
        "red": {
            "initial": {"target": "walk", "actions": "beep"},
            "states": {"walk": {}, "wait": {}, "stop": {}}
        }
    }
})");
const xstate::result<xstate::machine> light = xstate::create_machine(config, {});
if (!light.has_value()) {
    return 1;
}
const xstate::snapshot green = xstate::get_initial_snapshot(*light);
std::cout << boost::json::serialize(green.value) << '\n';

const xstate::event timer{.type = "timer", .payload = {}};
const xstate::snapshot yellow = xstate::get_next_snapshot(*light, green, timer);
std::cout << boost::json::serialize(yellow.value) << '\n';

const auto [walk, walk_actions] = xstate::transition(*light, yellow, timer);
std::cout << boost::json::serialize(walk.value) << ' ' << types_of(walk_actions) << '\n';

const xstate::event emergency{.type = "emergency", .payload = {}};
const auto [stop, stop_actions] = xstate::transition(*light, yellow, emergency);
std::cout << boost::json::serialize(stop.value) << ' ' << types_of(stop_actions) << '\n';

It prints:

"green"
"yellow"
{"red":"walk"} ["beep"]
{"red":"stop"} []

timer targets red, so the machine enters red and its initial child walk. initial may be an object, {"target", "actions"}: its actions run when the parent is entered through its initial state, and only then. So timer returns beep, and emergency, which targets red.stop directly, does not. beep has no implementation, so it comes back to the caller to execute (Actions), and types_of lists the types of the actions a step returned. Both transitions start from the same yellow snapshot: a snapshot is a value, and nothing stops a caller from computing two futures of it.

A parent state must name its initial state, even when every transition targets one of its children, and the name must be one of its children. create_machine refuses the machine otherwise:

const boost::json::value without_initial = boost::json::parse(R"({
    "initial": "red",
    "states": {
        "red": {"states": {"walk": {}, "wait": {}}}
    }
})");
const xstate::result<xstate::machine> first = xstate::create_machine(without_initial, {});
if (first.has_value()) {
    return 1;
}
std::cout << first.error().message() << '\n';

const boost::json::value unknown_initial = boost::json::parse(R"({
    "initial": "nowhere",
    "states": {"red": {}}
})");
const xstate::result<xstate::machine> second = xstate::create_machine(unknown_initial, {});
if (second.has_value()) {
    return 1;
}
std::cout << second.error().message() << '\n';

It prints:

invalid_config
unknown_target

The first config, a parent without initial, is invalid_config; the second, an initial that names no child, is unknown_target. XState’s createMachine refuses the first too, and accepts the second, which throws only when the state is first entered (A config is refused when it is created). xstate refuses both when the machine is created, with every other config error it can detect there, so a mistake in a config shows before the machine runs. Two are left to run time: the #id of an xstate.stateIn guard that names no state, found when the guard is evaluated (Higher-order guards and stateIn), and the default of a history state that is the machine’s root (Shallow and deep history).

Parent states

A state can contain states, its children, which are active only while it is. A parent state is also called a compound state. Grouping states under a parent lets them share what is common to them: a transition every child takes, an entry or exit action, a sub-process with its own initial state and its own end.

The machine is a parent state too, the root, and it is always active. A machine may have no states besides the root: its transitions are then the root’s, which run actions and target no state, and its state value is {} (The state value).

const boost::json::value config = boost::json::parse(R"({
    "id": "machine",
    "initial": "parent",
    "states": {
        "parent": {
            "initial": "child1",
            "states": {
                "child1": {"on": {"next": "child2"}},
                "child2": {
                    "initial": "grandchild1",
                    "states": {"grandchild1": {}, "grandchild2": {}}
                }
            },
            "on": {
                "next": ".child2.grandchild2",
                "restart": ".child1"
            }
        }
    }
})");
const xstate::result<xstate::machine> nested = xstate::create_machine(config, {});
if (!nested.has_value()) {
    return 1;
}
xstate::snapshot now = xstate::get_initial_snapshot(*nested);
std::cout << boost::json::serialize(now.value) << '\n';

const xstate::event next{.type = "next", .payload = {}};
now = xstate::get_next_snapshot(*nested, now, next);  // child1's own transition
std::cout << boost::json::serialize(now.value) << '\n';

now = xstate::get_next_snapshot(*nested, now, next);  // the parent's transition
std::cout << boost::json::serialize(now.value) << '\n';

const xstate::event restart{.type = "restart", .payload = {}};
now = xstate::get_next_snapshot(*nested, now, restart);
std::cout << boost::json::serialize(now.value) << '\n';

It prints:

{"parent":"child1"}
{"parent":{"child2":"grandchild1"}}
{"parent":{"child2":"grandchild2"}}
{"parent":"child1"}

The first next is handled by child1, the deepest active state, whose own transition goes to child2 and so into its initial child grandchild1. The second next finds no transition in grandchild1 or child2, and reaches parent, whose transition targets grandchild2. restart targets child1 from the parent.

Three rules govern transitions on parent states:

  • An event is offered to the deepest active state first, then to its parent, and so on up to the root; the first one with an enabled transition for the event takes it (Selecting a transition).

  • A transition that targets a parent enters the parent’s initial state, and that state’s initial state, down to a state without children.

  • A parent’s transition targets its own descendants with a path that starts with a dot: .child1, .child2.grandchild2 (Targets).

A parent state also ends: when one of its final children is reached, the parent is done, and its onDone transition is taken (A parent state is done).

Parallel states

A parallel state’s children, its regions, are all active at the same time, where a parent state has one active child. Parallel states model aspects of a system that change independently, such as a player’s track and its volume. Entering a parallel state enters every region, exiting it exits every region, and an event reaches every region: each region with a transition for it takes it, all in one microstep. A parallel state has "type": "parallel" and needs no initial.

const boost::json::value config = boost::json::parse(R"({
    "id": "player",
    "type": "parallel",
    "states": {
        "track": {
            "initial": "paused",
            "states": {
                "paused": {"on": {"PLAY": "playing"}},
                "playing": {"on": {"STOP": "paused", "RESET": "paused"}}
            }
        },
        "volume": {
            "initial": "normal",
            "states": {
                "normal": {"on": {"MUTE": "muted"}},
                "muted": {"on": {"UNMUTE": "normal", "RESET": "normal"}}
            }
        }
    }
})");
const xstate::result<xstate::machine> player = xstate::create_machine(config, {});
if (!player.has_value()) {
    return 1;
}
xstate::snapshot now = xstate::get_initial_snapshot(*player);
std::cout << boost::json::serialize(now.value) << '\n';

now = xstate::get_next_snapshot(*player, now, xstate::event{.type = "PLAY", .payload = {}});
now = xstate::get_next_snapshot(*player, now, xstate::event{.type = "MUTE", .payload = {}});
std::cout << boost::json::serialize(now.value) << '\n';

const xstate::event reset{.type = "RESET", .payload = {}};
const std::vector<xstate::microstep> steps = xstate::get_microsteps(*player, now, reset);
if (steps.empty()) {
    return 1;
}
const xstate::snapshot& after = steps.back().snapshot;
std::cout << steps.size() << " microstep: " << boost::json::serialize(after.value) << '\n';

It prints:

{"track":"paused","volume":"normal"}
{"track":"playing","volume":"muted"}
1 microstep: {"track":"paused","volume":"normal"}

The value of a parallel state is an object with one member per region. PLAY reaches both regions and only track handles it; MUTE only volume. RESET is handled by both regions, in a single microstep, which get_microsteps shows: that microstep’s transitions lists the two transitions taken (The microsteps of an event).

A parallel state is done when every one of its regions is (A parallel state is done).

Final states

A final state, "type": "final", is a state where a piece of work is complete. A final child completes its parent; a final child of the root completes the machine. A machine may have several final states, or none, if it runs for as long as it lives.

A parent state is done

When a parent’s final child is entered, the parent is done: the machine raises the event xstate.done.state.<parent id>, and the parent’s onDone is a transition on that event. It is taken in the same macrostep, so the caller of transition sees only where it led.

const boost::json::value config = boost::json::parse(R"({
    "id": "coffee",
    "initial": "preparation",
    "states": {
        "preparation": {
            "initial": "weighing",
            "states": {
                "weighing": {"on": {"weighed": "grinding"}},
                "grinding": {"on": {"ground": "ready"}},
                "ready": {"type": "final"}
            },
            "onDone": "brewing"
        },
        "brewing": {}
    }
})");
const xstate::result<xstate::machine> coffee = xstate::create_machine(config, {});
if (!coffee.has_value()) {
    return 1;
}
const xstate::event weighed{.type = "weighed", .payload = {}};
const xstate::snapshot grinding =
    xstate::get_next_snapshot(*coffee, xstate::get_initial_snapshot(*coffee), weighed);
std::cout << boost::json::serialize(grinding.value) << '\n';

const xstate::event ground{.type = "ground", .payload = {}};
for (const xstate::microstep& step : xstate::get_microsteps(*coffee, grinding, ground)) {
    std::cout << "microstep: " << boost::json::serialize(step.snapshot.value) << '\n';
}

It prints:

{"preparation":"grinding"}
microstep: {"preparation":"ready"}
microstep: "brewing"

ground takes two microsteps, which get_microsteps returns one by one. The first enters ready, the final child; the second runs the raised event xstate.done.state.coffee.preparation, whose onDone transition leaves preparation for brewing. Each microstep also carries its event, step.event, which XState’s getMicrosteps does not return (The microsteps of an event). A final state may have an output, which travels in the done event (A final state’s output).

A parallel state is done

A parallel state is done when every one of its regions is in a final state: the machine then raises the parallel state’s own done event, and its onDone is taken.

const boost::json::value config = boost::json::parse(R"({
    "id": "coffee",
    "initial": "preparing",
    "states": {
        "preparing": {
            "type": "parallel",
            "states": {
                "grindBeans": {
                    "initial": "grindingBeans",
                    "states": {
                        "grindingBeans": {"on": {"BEANS_GROUND": "beansGround"}},
                        "beansGround": {"type": "final"}
                    }
                },
                "boilWater": {
                    "initial": "boilingWater",
                    "states": {
                        "boilingWater": {"on": {"WATER_BOILED": "waterBoiled"}},
                        "waterBoiled": {"type": "final"}
                    }
                }
            },
            "onDone": "makingCoffee"
        },
        "makingCoffee": {}
    }
})");
const xstate::result<xstate::machine> coffee = xstate::create_machine(config, {});
if (!coffee.has_value()) {
    return 1;
}
xstate::snapshot now = xstate::get_initial_snapshot(*coffee);
std::cout << boost::json::serialize(now.value) << '\n';

const xstate::event beans_ground{.type = "BEANS_GROUND", .payload = {}};
now = xstate::get_next_snapshot(*coffee, now, beans_ground);
std::cout << boost::json::serialize(now.value) << '\n';

const xstate::event water_boiled{.type = "WATER_BOILED", .payload = {}};
now = xstate::get_next_snapshot(*coffee, now, water_boiled);
std::cout << boost::json::serialize(now.value) << '\n';

It prints:

{"preparing":{"grindBeans":"grindingBeans","boilWater":"boilingWater"}}
{"preparing":{"grindBeans":"beansGround","boilWater":"boilingWater"}}
"makingCoffee"

After BEANS_GROUND one region is in its final state and the other is not, so preparing waits. WATER_BOILED completes the second region, and preparing is done.

When the machine is done

A final child of the root completes the machine, and so does a parallel root once each of its regions is in a final state. In the same microstep the snapshot’s status becomes done and its output is computed: the root’s output, a fixed value in the config, or one an implementation computes from the context and the done event (Output). The snapshot keeps the state value of the final state.

const boost::json::value config = boost::json::parse(R"({
    "id": "process",
    "initial": "running",
    "states": {
        "running": {"on": {"finish": "complete"}},
        "complete": {"type": "final"}
    },
    "output": {"message": "Process completed."}
})");
const xstate::result<xstate::machine> process = xstate::create_machine(config, {});
if (!process.has_value()) {
    return 1;
}
xstate::snapshot now = xstate::get_initial_snapshot(*process);
std::cout << boost::json::serialize(now.value) << ' ' << name_of(now.status) << '\n';

now = xstate::get_next_snapshot(*process, now, xstate::event{.type = "finish", .payload = {}});
std::cout << boost::json::serialize(now.value) << ' ' << name_of(now.status) << '\n';
if (now.status == xstate::status::done && now.output.has_value()) {
    std::cout << "output: " << boost::json::serialize(*now.output) << '\n';
}

It prints:

"running" active
"complete" done
output: {"message":"Process completed."}

finish takes the machine to complete, a final child of the root, so its status becomes done and its output is the config’s output; name_of, a function of the program, spells the status. As XState does, the machine then resolves the exit actions of every active node, in reverse document order (The snapshot): it applies those it executes itself, such as an assign, and returns the others to its caller. After them it returns one xstate.stopChild for each child its snapshot still lists, which the snapshot keeps; the core runs no child, so stopping it is the caller’s work (Stopping a child).

A machine that is done receives no more events: an actor stops there (Actors). The pure functions do not check the status, as XState’s do not: from a done snapshot, get_next_snapshot takes the transition an event selects, and the status stays done. A caller that drives the core itself stops sending events once the status is done.

A final state should have no transitions, as Stately’s page on final states says, but neither XState 5.33.2 nor xstate enforces it: a final state that declares a transition takes it. Leave final states without transitions.

History states

A history state is a pseudostate: it is never active itself. A transition that targets it enters, instead, the children its parent had active when the parent was last exited, so a machine can resume where it left off. When the parent has never been exited, it enters the history state’s target, when it has one, or else the parent’s initial state, or, when the parent is a parallel state, every one of its regions.

const boost::json::value config = boost::json::parse(R"({
    "id": "checkout",
    "initial": "cart",
    "states": {
        "cart": {"on": {"checkout": "payment.hist"}},
        "payment": {
            "initial": "card",
            "states": {
                "card": {"on": {"switch": "paypal"}},
                "paypal": {"on": {"switch": "card"}},
                "hist": {"type": "history"}
            },
            "on": {"next": "address"}
        },
        "address": {"on": {"back": "payment.hist"}}
    }
})");
const xstate::result<xstate::machine> checkout = xstate::create_machine(config, {});
if (!checkout.has_value()) {
    return 1;
}
xstate::snapshot now = xstate::get_initial_snapshot(*checkout);
std::cout << boost::json::serialize(now.value) << '\n';

for (const std::string_view type : {"checkout", "switch", "next"}) {
    const xstate::event happened{.type = std::string(type), .payload = {}};
    now = xstate::get_next_snapshot(*checkout, now, happened);
    std::cout << type << ": " << boost::json::serialize(now.value) << '\n';
}

const std::vector<std::size_t>* remembered = now.remembered("checkout.payment.hist");
if (remembered == nullptr) {
    return 1;
}
boost::json::array ids;
for (const std::size_t node : *remembered) {
    ids.emplace_back(checkout->node(node).id);
}
std::cout << "remembered: " << boost::json::serialize(ids) << '\n';

now = xstate::get_next_snapshot(*checkout, now, xstate::event{.type = "back", .payload = {}});
std::cout << "back: " << boost::json::serialize(now.value) << '\n';

It prints:

"cart"
checkout: {"payment":"card"}
switch: {"payment":"paypal"}
next: "address"
remembered: ["checkout.payment.paypal"]
back: {"payment":"paypal"}

checkout targets payment.hist before payment was ever exited, so the machine enters the initial state of payment, card. next exits payment while paypal is active, and the history state records it; back returns to paypal. The snapshot’s history holds what each history state remembers, and remembered(id) returns it for one history node, as node indexes, or nullptr before the history state has recorded anything; XState’s is historyValue[id], the node objects.

A history state is "type": "history", and shallow unless it says "history": "deep"; "history": "shallow" and "history": true say shallow. A node with no type, no children and a history member is a history state too, when that member is truthy as JavaScript reads it, as in XState: a history of false, null, "" or 0 leaves the node atomic.

Shallow and deep history

A shallow history state remembers its parent’s active children only: entering one of them again enters that child’s initial state. A deep history state remembers the active states without children below its parent, so the whole nested configuration comes back.

/** The ids of the nodes a history node remembers, none when it has not recorded. */
boost::json::array remembered_ids(const xstate::machine& owner, const xstate::snapshot& now,
                                  std::string_view history_id) {
    const std::vector<std::size_t>* remembered = now.remembered(history_id);
    if (remembered == nullptr) {
        return {};
    }
    boost::json::array ids;
    for (const std::size_t node : *remembered) {
        ids.emplace_back(owner.node(node).id);
    }
    return ids;
}

const boost::json::value config = boost::json::parse(R"({
    "id": "player",
    "initial": "powered",
    "states": {
        "powered": {
            "initial": "radio",
            "states": {
                "radio": {
                    "initial": "stopped",
                    "states": {
                        "stopped": {"on": {"play": "playing"}},
                        "playing": {"on": {"stop": "stopped"}}
                    }
                },
                "shallow": {"type": "history"},
                "deep": {"type": "history", "history": "deep"}
            },
            "on": {"power": "standby"}
        },
        "standby": {
            "on": {
                "resume": "powered.shallow",
                "resumeDeep": "powered.deep"
            }
        }
    }
})");
const xstate::result<xstate::machine> player = xstate::create_machine(config, {});
if (!player.has_value()) {
    return 1;
}
xstate::snapshot standby = xstate::get_initial_snapshot(*player);
for (const std::string_view type : {"play", "power"}) {
    const xstate::event happened{.type = std::string(type), .payload = {}};
    standby = xstate::get_next_snapshot(*player, standby, happened);
}
const boost::json::array shallow = remembered_ids(*player, standby, "player.powered.shallow");
const boost::json::array deep = remembered_ids(*player, standby, "player.powered.deep");
std::cout << "shallow remembers " << shallow << '\n';
std::cout << "deep remembers " << deep << '\n';

for (const std::string_view type : {"resume", "resumeDeep"}) {
    const xstate::event resume{.type = std::string(type), .payload = {}};
    const xstate::snapshot resumed = xstate::get_next_snapshot(*player, standby, resume);
    std::cout << type << ": " << boost::json::serialize(resumed.value) << '\n';
}

It prints:

shallow remembers ["player.powered.radio"]
deep remembers ["player.powered.radio.playing"]
resume: {"powered":{"radio":"stopped"}}
resumeDeep: {"powered":{"radio":"playing"}}

Both resumptions start from the same standby snapshot. The shallow history state remembers radio, so it enters radio and its initial child stopped; the deep one remembers radio.playing, and enters it.

Three more rules:

  • A history state whose default leads back to it, a target that is an ancestor whose initial chain reaches the history state again, would never finish entering: create_machine refuses it with invalid_config, where XState overflows its stack when it enters it.

  • A machine whose root is itself a history state has no parent to resolve its default against. The machine is created, and a microstep that needs the default, the first one included, fails with unknown_target, where XState’s initial snapshot reports a TypeError as its error (A config is refused when it is created, Failures are values).

  • resolve_state builds a snapshot from a state value and a context, and from nothing else, so none of its history states has recorded anything. XState restores what they remember through the persistence of its actors, which xstate does not port (Persisted state). When what a history state remembers must survive a pause, keep the snapshot itself, a value the program holds between two events.

Tags

A tag is a name a state carries to say what kind of state it is, such as loading or visible: several states may share it, and a state may carry several. A state lists its tags under tags, one string or a list of strings; create_machine refuses any other tags, null included, with invalid_config (What creation refuses). The snapshot’s tags holds the tags of every active node, each once, and has_tag(tag) says whether one of them is the tag given, as XState’s snapshot.hasTag does.

/** Prints a snapshot's value, its tags, and whether one of them is "visible". */
void show(const xstate::snapshot& now) {
    const boost::json::array tags(now.tags.begin(), now.tags.end());
    std::cout << boost::json::serialize(now.value) << ' ' << tags << " visible: " << std::boolalpha
              << now.has_tag("visible") << '\n';
}

const boost::json::value config = boost::json::parse(R"({
    "id": "feedback",
    "initial": "prompt",
    "states": {
        "prompt": {"tags": ["visible"], "on": {"feedback.good": "thanks"}},
        "thanks": {
            "tags": ["visible"],
            "initial": "celebrating",
            "states": {"celebrating": {"tags": ["visible", "confetti"]}},
            "on": {"feedback.close": "closed"}
        },
        "closed": {"tags": "hidden"}
    }
})");
const xstate::result<xstate::machine> feedback = xstate::create_machine(config, {});
if (!feedback.has_value()) {
    return 1;
}
xstate::snapshot now = xstate::get_initial_snapshot(*feedback);
show(now);

const xstate::event good{.type = "feedback.good", .payload = {}};
now = xstate::get_next_snapshot(*feedback, now, good);
show(now);

const xstate::event close{.type = "feedback.close", .payload = {}};
now = xstate::get_next_snapshot(*feedback, now, close);
show(now);

It prints:

"prompt" ["visible"] visible: true
{"thanks":"celebrating"} ["visible","confetti"] visible: true
"closed" ["hidden"] visible: false

In thanks, both thanks and its child celebrating carry visible, and the snapshot lists it once, in the order of the active nodes. closed writes its one tag as a string. A tag answers what the machine is doing without naming where it is: code that asks has_tag("visible") keeps working when the states are renamed or nested differently, where a test of a state value would need to change. XState types its tags with setup({ types }); a JSON config has no types, so xstate’s tags are any strings.

Meta and descriptions

meta is static data a state carries for whoever reads the machine, such as the text a view shows in that state: any JSON, which the machine never reads itself. get_meta(machine, snapshot) collects the meta of every active node that has one, by the node’s id, as XState’s snapshot.getMeta() does. It takes the machine because the meta belongs to the nodes, and the snapshot names its nodes by index.

A description is a string that documents a state for the people who read the machine; Stately’s editor shows it beside the state. A transition may have a description and a meta too. xstate keeps each where the config put it, a state’s description in state_node::description, and writes them in the machine’s definition (The definition), except a state’s meta that is null and a state’s description that is not a string, which it drops (A state’s null meta, and a description that is not a string).

const boost::json::value config = boost::json::parse(R"({
    "id": "feedback",
    "initial": "prompt",
    "meta": {"title": "Feedback"},
    "states": {
        "prompt": {
            "description": "Waits for the user to rate the experience.",
            "meta": {"content": "How was your experience?"},
            "on": {"feedback.good": "thanks"}
        },
        "thanks": {"meta": {"content": "Thank you for your feedback!"}}
    }
})");
const xstate::result<xstate::machine> feedback = xstate::create_machine(config, {});
if (!feedback.has_value()) {
    return 1;
}
xstate::snapshot now = xstate::get_initial_snapshot(*feedback);
std::cout << xstate::get_meta(*feedback, now) << '\n';

const xstate::event good{.type = "feedback.good", .payload = {}};
now = xstate::get_next_snapshot(*feedback, now, good);
std::cout << xstate::get_meta(*feedback, now) << '\n';

const xstate::result<std::size_t> prompt = feedback->node_by_id("feedback.prompt");
if (!prompt.has_value()) {
    return 1;
}
std::cout << feedback->node(*prompt).description.value_or("") << '\n';

It prints:

{"feedback":{"title":"Feedback"},"feedback.prompt":{"content":"How was your experience?"}}
{"feedback":{"title":"Feedback"},"feedback.thanks":{"content":"Thank you for your feedback!"}}
Waits for the user to rate the experience.

The root’s meta is in both results, because the root is always active; the meta of prompt and then of thanks follows the active state. The description of prompt is read from its node, an optional string.

Context

A machine’s finite states say which mode it is in: idle or loading, inactive or active. Most machines also carry data that no finite set of states holds well: a count, the text of a form, a user’s id, the last error. That data is the machine’s context, which statecharts call extended state. It lives in the snapshot, beside the state value, and it is available in every state.

The context never changes in place. An assign action computes a new one, the microstep that runs it returns a new snapshot that holds it, and the snapshot it began from keeps the old one. The context is optional: a machine that only needs its finite states leaves it out, and its snapshots then hold an empty object.

In xstate the context is JSON, a boost::json::value, and in practice an object. The smallest example is a machine with an initial context, and the snapshot it starts in:

const boost::json::value config = boost::json::parse(R"({
    "id": "feedback",
    "context": {"feedback": "Some feedback", "rating": 5}
})");
const xstate::result<xstate::machine> feedback_machine = xstate::create_machine(config, {});
if (!feedback_machine.has_value()) {
    return 1;
}
const xstate::snapshot snapshot = xstate::get_initial_snapshot(*feedback_machine);
std::cout << boost::json::serialize(snapshot.context) << '\n';

It prints:

{"feedback":"Some feedback","rating":5}

The config’s context is the one XState’s createMachine takes, written in JSON. get_initial_snapshot, XState’s getInitialSnapshot, computes the snapshot a machine starts in (Pure transitions and the macrostep cursor), and the snapshot’s context is a boost::json::value, which boost::json::serialize writes. Stately’s own page reads the context through an actor and its subscription; that program is in Context in an actor.

Initial context

The config’s context is the initial context of every snapshot the machine starts in. xstate copies it into each one, so every snapshot owns its context and no two snapshots, or two actors, share one. XState hands every actor the config’s object itself, which is why Stately’s page warns that mutating it may change the context of other actors; a boost::json::value held by value leaves nothing for that warning to guard.

A machine without a context, or with "context": null, starts with an empty object, as XState’s does:

const boost::json::value no_context = boost::json::parse(R"({"id": "toggle"})");
const xstate::result<xstate::machine> toggle = xstate::create_machine(no_context, {});
if (!toggle.has_value()) {
    return 1;
}
std::cout << boost::json::serialize(xstate::get_initial_snapshot(*toggle).context) << '\n';

It prints:

{}

Write the context as an object. xstate keeps any other value as written, false, a number, a string or a list, and an assign on it fails with implementation_failed. XState starts a context of false, 0 or "" as {}, and its assign, which merges with JavaScript’s Object.assign, drops a context of true or a number and spreads a string or a list into numbered members (A context that is not an object).

Lazy initial context

Some context is not known when the machine is written: a timestamp, an id, anything that must be computed when the machine starts. XState takes the context as a function for that, which runs once for each actor, when the actor is created, so that each actor computes its own. A function cannot be JSON, so xstate takes it from the implementations, as it takes all the code a machine names: implementations.context, a context_maker. When it is set, it replaces the config’s context.

It runs at the start of every initial macrostep: in each get_initial_snapshot, initial_transition, get_initial_microsteps and begin_initial, and when an actor is constructed. A child is constructed when its parent resolves the spawn of an invoke or a spawn_child_action. A root is constructed on start, not on create_actor, which runs nothing; XState’s createActor runs the function at once (The root is constructed on start).

/** The caller's clock, in milliseconds: xstate reads none. */
std::uint64_t now = 1000;
xstate::implementations implementations;
implementations.context =
    [&now](const boost::json::value& /*input*/) -> xstate::result<boost::json::value> {
    return boost::json::object{{"feedback", "Some feedback"}, {"createdAt", now}};
};
const boost::json::value config = boost::json::parse(R"({"id": "feedback"})");
const xstate::result<xstate::machine> feedback_machine =
    xstate::create_machine(config, implementations);
if (!feedback_machine.has_value()) {
    return 1;
}

now = 2000;
const xstate::snapshot first = xstate::get_initial_snapshot(*feedback_machine);
now = 3000;
const xstate::snapshot second = xstate::get_initial_snapshot(*feedback_machine);
std::cout << boost::json::serialize(first.context) << '\n'
          << boost::json::serialize(second.context) << '\n';

It prints:

{"feedback":"Some feedback","createdAt":2000}
{"feedback":"Some feedback","createdAt":3000}

The machine was created while now was 1000, and each snapshot holds the time at which it was computed. xstate itself never reads a clock, and a context_maker that read one would make the initial snapshot depend on the moment it ran, so the time here comes from a variable the function captures, where Stately’s page reads Date.now(). A program whose results must follow from what it is given passes the time in the input (Initial context from the input).

Initial context from the input

The input is the data a machine is started with, as a function is called with its arguments; Input and output covers it whole. The initial context is often computed from it: a user’s id, a default rating. XState’s context function receives { input }. xstate’s context_maker receives the input itself, a boost::json::value, null when none was given. The input is the second argument of get_initial_snapshot, initial_transition, get_initial_microsteps and begin_initial, and the input of an actor’s actor_options.

xstate::result<boost::json::value> initial_feedback(const boost::json::value& input) {
    const boost::json::object* given = input.if_object();
    const boost::json::value* rating =
        given == nullptr ? nullptr : given->if_contains("defaultRating");
    if (rating == nullptr || !rating->is_number()) {
        return xstate::failure<boost::json::value>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"feedback", ""}, {"rating", *rating}};
}

xstate::implementations implementations;
implementations.context = initial_feedback;
const boost::json::value config = boost::json::parse(R"({"id": "feedback"})");
const xstate::result<xstate::machine> feedback_machine =
    xstate::create_machine(config, implementations);
if (!feedback_machine.has_value()) {
    return 1;
}

const boost::json::value input = boost::json::parse(R"({"defaultRating": 5})");
const xstate::snapshot snapshot = xstate::get_initial_snapshot(*feedback_machine, input);
std::cout << boost::json::serialize(snapshot.context) << '\n';

It prints:

{"feedback":"","rating":5}

initial_feedback checks the input before it reads it, and returns an error when the input lacks what it needs: xstate throws no exception, and where XState’s function throws, xstate’s returns the error as a value. A context that cannot be computed fails the initial macrostep on its first next(), before any state is entered: the snapshot’s status is error, its error the code the function returned, and its context an empty object, the snapshot XState’s getInitialSnapshot returns when its function throws. The same machine without an input:

const xstate::snapshot without = xstate::get_initial_snapshot(*feedback_machine);
std::cout << (without.status == xstate::status::error ? "error " : "active ")
          << boost::json::serialize(without.context) << '\n';

It prints:

error {}

An actor takes the same input in its options, as XState’s createActor(machine, { input }) does (Options):

xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> feedback_actor =
    system.create_actor(*feedback_machine, {.input = input});
if (!feedback_actor.has_value()) {
    return 1;
}
if (!system.start(*feedback_actor).has_value()) {
    return 1;
}
const xstate::snapshot* started = system.snapshot_of(*feedback_actor);
if (started == nullptr) {
    return 1;
}
std::cout << boost::json::serialize(started->context) << '\n';

It prints:

{"feedback":"","rating":5}

Three rules hold for the function:

  • It returns an object. Any other value fails the initial macrostep with implementation_failed, where XState merges whatever its function returns into an empty object (A context that is not an object).

  • It takes the input only. XState also hands it spawn and self; an xstate context cannot spawn a child (The context function).

  • An empty std::function is refused by create_machine with invalid_config, because calling it would abort in a build without exceptions.

Updating context with assign

The context changes through one action: assign. Like any action, it is named by a transition, an entry or an exit, and runs when that transition is taken or that state entered or left (Assign). Its implementation is an assigner: a function of the action_args that returns a partial update, an object. The update is merged into the context as Object.assign merges it: each of its members replaces the context’s member of that name, or is added, and every other member is kept.

XState writes an assign in the config or in the actions of setup. xstate registers it by name, as an assign_action in implementations.actions, because a config is JSON and a function is not. Here update_feedback copies the event’s feedback into the context:

xstate::result<boost::json::object> update_feedback(const xstate::action_args& args) {
    const boost::json::value* feedback = args.event.payload.if_contains("feedback");
    if (feedback == nullptr) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"feedback", *feedback}};
}

xstate::implementations implementations;
implementations.actions.emplace("updateFeedback",
                                xstate::assign_action{.assignment = update_feedback});
const boost::json::value config = boost::json::parse(R"({
    "id": "feedback",
    "context": {"feedback": "Some feedback", "rating": 5},
    "on": {"feedback.update": {"actions": "updateFeedback"}}
})");
const xstate::result<xstate::machine> feedback_machine =
    xstate::create_machine(config, implementations);
if (!feedback_machine.has_value()) {
    return 1;
}

const xstate::snapshot initial = xstate::get_initial_snapshot(*feedback_machine);
const xstate::event update{
    .type = "feedback.update",
    .payload = {{"feedback", "Some other feedback"}},
};
const auto [next, actions] = xstate::transition(*feedback_machine, initial, update);
std::cout << boost::json::serialize(initial.context) << '\n'
          << boost::json::serialize(next.context) << '\n'
          << actions.size() << " actions\n";

It prints:

{"feedback":"Some feedback","rating":5}
{"feedback":"Some other feedback","rating":5}
0 actions

The update named feedback only, so rating was kept. The event’s members other than type are its payload, args.event.payload (Events). transition returned no action: the library runs an assign while it computes the microstep and never returns it to the caller, as XState does. The snapshot initial still holds the context it had.

XState’s assign takes either a function that returns the update, assign(({ context, event }, params) => ({ ... })), or an object of property assigners and fixed values. An xstate assigner is the function form: it builds every member of the update itself, as update_feedback builds feedback. XState’s assigner also receives spawn, self and system, and can put an actor it spawns in the context. An xstate assigner receives only the action_args, a context holds no actor, and a child is spawned by a spawn_child_action (Spawning actors, The context function).

The rules of an assign:

  • The merge is shallow, as the merge of Object.assign is: a member that holds an object is replaced whole, not merged.

  • An update adds and replaces members; it removes none. Set a member to null to clear it.

  • The context must be an object (Initial context).

  • An assigner that returns an error fails the macrostep, and no assign of that macrostep reaches the context (A snapshot is a value).

  • create_machine refuses an assign_action whose assignment is empty, with invalid_config.

How guards and actions read the context

Every function of the implementations but the context function, which receives the input (Initial context from the input), receives an action_args: context, the context as it is when the implementation runs; event, the microstep’s event; and params, the params the config gave the action or the guard, null when it gave none. These are the three XState hands an implementation, the first two in its first argument and params in its second. The machine’s output is the one exception to event, as in XState: it receives the done event that completed the machine, that of the top-level final state that completed it or, for a parallel root, the root’s own (The machine’s output and its final state).

  • A guard runs while its transition is selected, before any action of the microstep, and reads the context of the snapshot the microstep starts from.

  • An action reads the context as the actions before it in the same microstep left it, in XState’s order: the exit actions, the transitions’ actions, the entry actions (Actions). Two assigns in one microstep apply in order, each to the context the one before it left.

  • The eventless transitions are selected again after the microstep of an event, and after the microstep of an eventless transition when it changed the snapshot; a microstep that ran an assign has changed it, whatever values the assign wrote. So an always guarded on the context sees, in the same macrostep, the context an assign left (Eventless transitions).

A counter shows the first two. belowLimit guards inc, add adds its param by, and report is a log whose value is the count it sees. Each reads its numbers with integer_at, which returns the integer an object holds under a key, or none:

/** The integer `object` holds under `key`, or none when it holds no integer there. */
std::optional<std::int64_t> integer_at(const boost::json::value& object, std::string_view key) {
    const boost::json::object* members = object.if_object();
    const boost::json::value* found = members == nullptr ? nullptr : members->if_contains(key);
    if (found == nullptr || !found->is_int64()) {
        return std::nullopt;
    }
    return found->get_int64();
}

xstate::result<bool> below_limit(const xstate::action_args& args) {
    const std::optional<std::int64_t> count = integer_at(args.context, "count");
    const std::optional<std::int64_t> limit = integer_at(args.context, "limit");
    if (!count.has_value() || !limit.has_value()) {
        return xstate::failure<bool>(xstate::errc::implementation_failed);
    }
    return *count < *limit;
}

xstate::result<boost::json::object> add(const xstate::action_args& args) {
    const std::optional<std::int64_t> count = integer_at(args.context, "count");
    const std::optional<std::int64_t> by = integer_at(args.params, "by");
    if (!count.has_value() || !by.has_value()) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"count", *count + *by}};
}

xstate::result<std::optional<boost::json::value>> report(const xstate::action_args& args) {
    const std::optional<std::int64_t> count = integer_at(args.context, "count");
    if (!count.has_value()) {
        return xstate::failure<std::optional<boost::json::value>>(
            xstate::errc::implementation_failed);
    }
    return std::optional<boost::json::value>(*count);
}

xstate::implementations implementations;
implementations.guards.emplace("belowLimit", below_limit);
implementations.actions.emplace("add", xstate::assign_action{.assignment = add});
implementations.actions.emplace("report",
                                xstate::log_action{.value = report, .label = std::nullopt});
const boost::json::value config = boost::json::parse(R"({
    "id": "counter",
    "context": {"count": 0, "limit": 2},
    "on": {
        "inc": {
            "guard": "belowLimit",
            "actions": ["report", {"type": "add", "params": {"by": 1}}, "report"]
        }
    }
})");
const xstate::result<xstate::machine> counter = xstate::create_machine(config, implementations);
if (!counter.has_value()) {
    return 1;
}

xstate::snapshot now = xstate::get_initial_snapshot(*counter);
for (int sent = 0; sent < 3; ++sent) {
    auto [next, actions] =
        xstate::transition(*counter, now, xstate::event{.type = "inc", .payload = {}});
    boost::json::array logged;
    for (const xstate::action& returned : actions) {
        const boost::json::object* params = returned.params.if_object();
        if (returned.type != "xstate.log" || params == nullptr) {
            continue;
        }
        if (const boost::json::value* value = params->if_contains("value")) {
            logged.push_back(*value);
        }
    }
    std::cout << boost::json::serialize(next.context) << " logged "
              << boost::json::serialize(logged) << '\n';
    now = std::move(next);
}

It prints:

{"count":1,"limit":2} logged [0,1]
{"count":2,"limit":2} logged [1,2]
{"count":2,"limit":2} logged []

Each inc that was taken logged the count twice: report ran before add and after it, and saw each context. The third inc found the count at the limit: the guard was false, no transition was taken, and nothing ran or was returned. report is returned as xstate.log, its value resolved from the context at its turn (Log).

A custom action, one the implementations do not hold, is returned to the caller with the params the config gave it, and nothing of the context, where XState’s carries the context it ran with in its info (Custom actions). The code that executes it reads the context from a snapshot, which holds it as a whole microstep left it: the snapshot of the microstep that returned the action, from the cursor or get_microsteps, holds the context after every action of that microstep (Microsteps). A value that must be computed from the context at the action’s turn is computed by a registered implementation, as the value of report is here.

A snapshot is a value

A snapshot is a plain C++ value: the state value, the context, the status and the rest. A transition never changes the snapshot it begins from; it returns a new one. The same event taken twice from the same snapshot gives two snapshots equal member by member, and the first is still there, to be used again or stored. A counter whose inc adds one, and whose set increments, then assigns the count its event carries, and fails without one:

xstate::result<boost::json::object> increment(const xstate::action_args& args) {
    const boost::json::object* context = args.context.if_object();
    const boost::json::value* count = context == nullptr ? nullptr : context->if_contains("count");
    if (count == nullptr || !count->is_int64()) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"count", count->get_int64() + 1}};
}

xstate::result<boost::json::object> set_count(const xstate::action_args& args) {
    const boost::json::value* count = args.event.payload.if_contains("count");
    if (count == nullptr || !count->is_int64()) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"count", *count}};
}

xstate::implementations implementations;
implementations.actions.emplace("increment", xstate::assign_action{.assignment = increment});
implementations.actions.emplace("setCount", xstate::assign_action{.assignment = set_count});
const boost::json::value config = boost::json::parse(R"({
    "id": "counter",
    "context": {"count": 0},
    "on": {
        "inc": {"actions": "increment"},
        "set": {"actions": ["increment", "setCount"]}
    }
})");
const xstate::result<xstate::machine> counter = xstate::create_machine(config, implementations);
if (!counter.has_value()) {
    return 1;
}

const xstate::event inc{.type = "inc", .payload = {}};
const xstate::snapshot first = xstate::get_initial_snapshot(*counter);
const xstate::snapshot second = xstate::get_next_snapshot(*counter, first, inc);
const xstate::snapshot again = xstate::get_next_snapshot(*counter, first, inc);
std::cout << boost::json::serialize(first.context) << ' '
          << boost::json::serialize(second.context) << ' '
          << boost::json::serialize(again.context) << '\n';

It prints:

{"count":0} {"count":1} {"count":1}

A failure keeps the snapshot a macrostep began from too. When an implementation fails, the macrostep ends in that snapshot, its status error and its error the code the implementation returned: no assign of the failed macrostep reaches the context, not even one that ran before the failure. That is the snapshot XState’s actor keeps, where its pure functions throw (When an implementation fails, Failures are values). A set without a count, from the snapshot second:

const xstate::snapshot failed =
    xstate::get_next_snapshot(*counter, second, xstate::event{.type = "set", .payload = {}});
std::cout << (failed.status == xstate::status::error ? "error " : "active ")
          << boost::json::serialize(failed.context) << '\n';

It prints:

error {"count":1}

The count is 1, the count of second: increment ran and made it 2, then setCount failed, and the increment was dropped with the rest of the macrostep.

Since a snapshot is a value, its caller keeps it where it likes: in a variable, a queue, a map of sessions. Its state value and its context are JSON, so they can be stored, in a database or across two requests, and resolve_state(machine, value, context) builds a snapshot back from them, as XState’s machine.resolveState({ value, context }) does. It returns a result and refuses a value that names no state with unknown_state. It restores the value and the context and nothing else: the status comes from the value, done or active, never error, and the output, what history states remember and the children are not restored (Persisted state). Inside a macrostep, the snapshot of each microstep holds the context as that microstep left it, and the macrostep cursor hands each one to its caller (The macrostep cursor).

Context in an actor

An actor keeps the snapshot for its caller: it starts in the initial snapshot, takes each event sent to it, and keeps the snapshot each macrostep settles in. Stately’s page shows the context through an actor that a listener watches. Here the actor is one of xstate’s actor_system, and its machine updates the context with update_feedback, the assigner of Updating context with assign:

xstate::implementations implementations;
implementations.actions.emplace("updateFeedback",
                                xstate::assign_action{.assignment = update_feedback});
const boost::json::value config = boost::json::parse(R"({
    "id": "feedback",
    "context": {"feedback": "Some feedback"},
    "on": {"feedback.update": {"actions": "updateFeedback"}}
})");
const xstate::result<xstate::machine> feedback_machine =
    xstate::create_machine(config, implementations);
if (!feedback_machine.has_value()) {
    return 1;
}

xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> feedback_actor = system.create_actor(*feedback_machine);
if (!feedback_actor.has_value()) {
    return 1;
}
const xstate::result<void> subscribed =
    system.subscribe(*feedback_actor, [](const xstate::snapshot& state) {
        std::cout << boost::json::serialize(state.context) << '\n';
    });
if (!subscribed.has_value()) {
    return 1;
}
if (!system.start(*feedback_actor).has_value()) {
    return 1;
}
const xstate::event update{
    .type = "feedback.update",
    .payload = {{"feedback", "Some other feedback"}},
};
if (!system.send(*feedback_actor, update).has_value()) {
    return 1;
}

It prints:

{"feedback":"Some feedback"}
{"feedback":"Some other feedback"}

subscribe hands its listener each snapshot the actor settles in while it is active, and the one it is done in, as XState’s subscribe does; snapshot_of returns the latest. The listener runs inside the actor’s turn: it may read the system, but a call from it that changes the system, a send among them, is refused with xactor’s invalid_argument and changes nothing, where XState accepts a send from a subscriber and handles it after the update in progress (A callback that calls the system back). Actors, their fuel (Fuel, parking and resume) and their children are the subject of Actors.

Context and types

XState types the context with TypeScript, in setup({ types: { context } }), and the compiler checks every implementation against it. An xstate machine is loaded from JSON at run time, so no type exists for a compiler to check: the context is a boost::json::value, and each implementation checks what it reads, before it reads it. It returns an error when a member is missing or of the wrong kind, as integer_at and its callers do in How guards and actions read the context. That error is a value: it ends the macrostep in the snapshot’s status (A snapshot is a value).

Read members with if_object and if_contains, which return a null pointer when the value is not an object or the member is missing. at throws on a missing member, and in a build without exceptions the throw aborts the program.

Context cheatsheet

XState xstate

context: { feedback: '' }

"context": {"feedback": ""} in the config

context: () => ({ ... })

implementations.context, a context_maker

context: ({ input }) => ({ ... })

the same, reading its const boost::json::value& input

getInitialSnapshot(machine, input)

get_initial_snapshot(machine, input)

createActor(machine, { input })

system.create_actor(machine, {.input = input})

assign({ feedback: ({ event }) => event.feedback })

an assign_action whose assignment returns {"feedback": ...}, read from args.event.payload

assign(({ context, event }, params) => ({ ... }))

an assign_action whose assignment returns the partial update

context, event and params in an implementation

args.context, args.event (its type, and its other members in args.event.payload) and args.params

snapshot.context

snapshot.context, a boost::json::value

setup({ types: { context } })

none: each implementation checks what it reads

Events and transitions

A transition is a change from one state to another. An event is what causes it: a message the machine receives, with a type that names what happened and the data that travels with it. When a machine receives an event, it looks in its active states for the transitions that event enables. If there are some, it takes them: it leaves the states they leave, runs their actions and enters the states they reach. If there is none, nothing changes.

Transitions are deterministic: the same snapshot and the same event always lead to the same next snapshot and the same actions. That is what lets a step be computed as a plain function, tested event by event, and replayed.

This section covers the events a machine receives, the transitions of a state’s on and where they lead, how an event selects among them, event descriptors and wildcards, guards, and the two kinds of transitions no event names: eventless (always) and delayed (after) ones. The actions a transition runs are in Actions; how each microstep runs, and how a caller pays for it, in Pure transitions and the macrostep cursor.

Events

An event is an object with a type, the string that names what happened, and any other members, its payload, as an event is in XState. xstate’s event holds the two apart: type, a std::string, and payload, a boost::json::object holding every other member. Transitions match the type; the payload is data for the implementations, which receive the event as args.event. The example reads an event from JSON with event_from_json, prints its two parts, and sends it to a machine:

const boost::json::value config = boost::json::parse(R"({
    "id": "feedback",
    "initial": "question",
    "states": {
        "question": {
            "on": {
                "feedback.good": {"target": "thanks"}
            }
        },
        "thanks": {}
    }
})");
const xstate::result<xstate::machine> feedback = xstate::create_machine(config, {});
if (!feedback.has_value()) {
    return 1;
}

const xstate::result<xstate::event> event = xstate::event_from_json(boost::json::parse(
    R"({"type": "feedback.good", "feedback": "This is great!", "rating": 5})"));
if (!event.has_value()) {
    return 1;
}
std::cout << event->type << ' ' << boost::json::serialize(event->payload) << '\n';

const xstate::snapshot question = xstate::initial_transition(*feedback).first;
const xstate::snapshot thanks = xstate::transition(*feedback, question, *event).first;
std::cout << boost::json::serialize(question.value) << " -> "
          << boost::json::serialize(thanks.value) << '\n';

It prints:

feedback.good {"feedback":"This is great!","rating":5}
"question" -> "thanks"

An event usually arrives as JSON, in the flat form XState’s actor.send takes, {"type": ..., ...payload}. event_from_json reads that form and to_json writes it, its type first. An event can also be built in C++, from its two members:

const xstate::event update{
    .type = "feedback.update",
    .payload = {{"feedback", "This is great!"}, {"rating", 5}},
};
std::cout << boost::json::serialize(xstate::to_json(update)) << '\n';

event_from_json refuses a value that is not an object, or whose type is missing or not a string, with invalid_event:

for (const std::string_view text : {
         R"({"type": "feedback.update", "rating": 5})",
         R"({"type": "*"})",
         R"({"rating": 5})",
         R"({"type": 5})",
         R"("feedback.update")",
     }) {
    boost::system::error_code parsed;
    const boost::json::value json = boost::json::parse(text, parsed);
    if (parsed) {
        return 1;
    }
    const xstate::result<xstate::event> read = xstate::event_from_json(json);
    if (!read.has_value()) {
        std::cout << text << " -> " << read.error().message() << '\n';
        continue;
    }
    std::cout << text << " -> " << read->type << ' ' << boost::json::serialize(read->payload)
              << '\n';
}

It prints:

{"type":"feedback.update","feedback":"This is great!","rating":5}
{"type": "feedback.update", "rating": 5} -> feedback.update {"rating":5}
{"type": "*"} -> * {}
{"rating": 5} -> invalid_event
{"type": 5} -> invalid_event
"feedback.update" -> invalid_event

An event of the type * is read like any other. A machine refuses it as the event of a macrostep, which a caller sends or a clock releases, and takes it only as an event it raised itself without a delay (An event of the type *).

Events whose type starts with xstate. are the machine’s own: xstate.init is the event of the initial macrostep, xstate.after.<delay>.<state id> comes back from a delayed transition (Delayed transitions), xstate.done.state.<state id> is raised when a compound or parallel state completes (Done events), and xstate.done.actor.<id> and xstate.error.actor.<id> come from invoked actors (Done and error events). They select transitions as any other event does, with two exceptions in XState that xstate does not follow yet. When xstate.init comes from outside, XState’s macrostep selects no transition for it, and when xstate.stop, the event that stops an XState actor, comes from outside, it stops the machine. xstate selects transitions for both as for any other event, so an on entry that names one, or a * transition, takes it. A caller should therefore not send xstate.init or xstate.stop, nor make up any other of these events. A caller of the machine core does deliver two kinds itself: each xstate.after... event its clock releases (In the machine core), and the done and error events of the children it runs, since the core runs none. The actor layer delivers both for its actors.

Transitions

A state’s on maps event descriptors (Event descriptors) to transitions. Each transition is written in one of three forms:

"on": {
    "feedback.good": "thanks",
    "feedback.bad": {"target": "form", "actions": "track"},
    "feedback.provide": [
        {"guard": "sentimentGood", "target": "thanks"},
        {"target": "form"}
    ]
}
  • a string, the shorthand for {"target": "thanks"};

  • an object with any of target, actions, guard, reenter, description and meta;

  • a list of strings and objects, tried in order until one is enabled (Guards).

target names the state or states the transition goes to (Targets); a transition without one is targetless. actions lists the actions it runs when it is taken (Actions), guard the condition it is taken on (Guards), and reenter whether it leaves and enters its source again (Self-transitions and reenter). description and meta are for readers and tools: xstate keeps them in the machine’s definition, which to_json writes as XState’s toJSON() does, and does nothing else with them. The root is a state too: its on handles an event in every state, unless a deeper state handles it first.

Creating the machine checks every transition, so a mistake is an error before any event runs: a target that names no state is refused with unknown_target, a guard the implementations do not hold with unknown_guard, and an on key that is the empty string with invalid_config, as XState refuses it and asks for always instead. invalid_config also refuses a transition that declares cond, XState v4’s name for guard, with a value JavaScript reads as true, as XState’s development build does, and an actions that is null, which XState accepts as a list that holds null, and then throws when it takes the transition (What creation refuses lists them all).

Selecting a transition

An event selects its transitions starting from the deepest active states:

  1. Start at each active atomic state, a state with no children.

  2. Take the first of its transitions for the event that is enabled: one without a guard, or whose guard passes.

  3. If it has none, look in its parent, and so on up to the root.

  4. If no state up to the root has one, the event changes nothing.

In a parallel state each region selects on its own, so one event can take a transition in each region (Parallel states). The parallel state’s own transitions, and its ancestors’, are tried only when no region selected one. When the transitions selected in two regions would exit a common state, as two that leave the parallel state would, only the first is taken, in the order the state value lists the regions.

In the editor below, escape means three things: in menu it closes the menu, in text it closes the editor, and in saving it does nothing.

const boost::json::value config = boost::json::parse(R"({
    "id": "editor",
    "initial": "editing",
    "states": {
        "editing": {
            "initial": "text",
            "on": {"escape": "closed"},
            "states": {
                "text": {"on": {"menu.open": "menu", "save": "saving"}},
                "menu": {"on": {"escape": "text"}},
                "saving": {"on": {"escape": {}}}
            }
        },
        "closed": {}
    }
})");
const xstate::result<xstate::machine> editor = xstate::create_machine(config, {});
if (!editor.has_value()) {
    return 1;
}

const std::vector<std::vector<std::string_view>> runs{
    {"menu.open", "escape", "escape"},
    {"save", "escape"},
};
for (const std::vector<std::string_view>& run : runs) {
    xstate::snapshot now = xstate::initial_transition(*editor).first;
    std::cout << "start " << boost::json::serialize(now.value) << '\n';
    for (const std::string_view type : run) {
        const xstate::event happened{.type = std::string(type), .payload = {}};
        now = xstate::transition(*editor, now, happened).first;
        std::cout << type << " -> " << boost::json::serialize(now.value) << '\n';
    }
}

It prints:

start {"editing":"text"}
menu.open -> {"editing":"menu"}
escape -> {"editing":"text"}
escape -> "closed"
start {"editing":"text"}
save -> {"editing":"saving"}
escape -> {"editing":"saving"}

In menu, its own escape is taken: the deepest state wins. In text, which has no escape, the parent’s is taken and the editor closes. saving declares escape with no target and no actions: a forbidden transition. It is selected, so the search stops there and the parent’s escape is never looked at, and it changes nothing: the editor cannot be closed while it saves. Leaving the event out of saving would have let the parent take it.

An event that selects no transition, or only a forbidden one, changes nothing in its own microstep. The eventless transitions are still selected after it, with that event (Eventless transitions), so the macrostep settles at its first microstep with the snapshot unchanged unless one of them now passes, for example one whose guard reads the event. can(machine, snapshot, event), XState’s snapshot.can(event), says beforehand whether an event would take a transition that has a target or actions (Whether an event would do anything); for escape in saving it says no.

Targets

A target is written relative to the state that declares the transition, its source:

Target Names

"b"

the sibling b of the source

"b.two"

the child two of the sibling b: a path of keys, separated by dots

".other"

the child other of the source; from the root, ".a" names the top-level state a

"#c"

the state whose id is c, anywhere in the machine; "#c.other" continues from it as a path

The root has no siblings, so a target declared on the root starts with . or #: "a" there is refused with unknown_target, as XState refuses it and suggests ".a".

A state’s id is its id member, or, without one, the machine’s id followed by the state’s path, joined by dots: in the machine below, #machine.b.two names the state two.

const boost::json::value config = boost::json::parse(R"({
    "id": "machine",
    "initial": "a",
    "on": {"reset": ".a"},
    "states": {
        "a": {"on": {"next": "b", "deep": "b.two"}},
        "b": {
            "initial": "one",
            "on": {"next": "#c"},
            "states": {"one": {}, "two": {}}
        },
        "c": {
            "id": "c",
            "initial": "child",
            "on": {"next": ".other"},
            "states": {"child": {}, "other": {}}
        }
    }
})");
const xstate::result<xstate::machine> machine = xstate::create_machine(config, {});
if (!machine.has_value()) {
    return 1;
}

xstate::snapshot now = xstate::initial_transition(*machine).first;
std::cout << "start " << boost::json::serialize(now.value) << '\n';
for (const std::string_view type : {"next", "next", "next", "reset", "deep"}) {
    const xstate::event happened{.type = std::string(type), .payload = {}};
    now = xstate::transition(*machine, now, happened).first;
    std::cout << type << " -> " << boost::json::serialize(now.value) << '\n';
}

It prints:

start "a"
next -> {"b":"one"}
next -> {"c":"child"}
next -> {"c":"other"}
reset -> "a"
deep -> {"b":"two"}

a goes to its sibling b, and entering b, a compound state, enters its initial child one. one has no next, so the next of b is taken, to the state whose id is c, which enters child. c then goes to its own child other. reset, on the root, returns to a from anywhere, and deep names a child of a sibling, so b is entered at two instead of its initial child.

Multiple targets

A transition may name several targets, as a list under target, when each is in a different region of a parallel state (Parallel states): it moves every region it names in one microstep. The list goes under target; a list in the place of the transition is a list of transitions, of which the first enabled is taken.

const boost::json::value config = boost::json::parse(R"({
    "id": "settings",
    "type": "parallel",
    "on": {
        "set.dark.custom": {"target": [".mode.dark", ".theme.custom"]},
        "set.light": {"target": ".mode.light"}
    },
    "states": {
        "mode": {
            "initial": "light",
            "on": {"mode.light": ".light"},
            "states": {
                "light": {"on": {"toggle": "dark"}},
                "dark": {"on": {"toggle": "light"}}
            }
        },
        "theme": {
            "initial": "default",
            "states": {
                "default": {"on": {"change": "custom"}},
                "custom": {"on": {"change": "default"}}
            }
        }
    }
})");
const xstate::result<xstate::machine> settings = xstate::create_machine(config, {});
if (!settings.has_value()) {
    return 1;
}

xstate::snapshot now = xstate::initial_transition(*settings).first;
std::cout << "start " << boost::json::serialize(now.value) << '\n';
for (const std::string_view type :
     {"set.dark.custom", "set.light", "set.dark.custom", "mode.light"}) {
    const xstate::event happened{.type = std::string(type), .payload = {}};
    now = xstate::transition(*settings, now, happened).first;
    std::cout << type << " -> " << boost::json::serialize(now.value) << '\n';
}

It prints:

start {"mode":"light","theme":"default"}
set.dark.custom -> {"mode":"dark","theme":"custom"}
set.light -> {"mode":"light","theme":"default"}
set.dark.custom -> {"mode":"dark","theme":"custom"}
mode.light -> {"mode":"light","theme":"custom"}

set.dark.custom moves both regions. set.light names one target, yet it also sends theme back to default. A transition leaves every active state inside its domain: its source, when every target is inside the source and it does not reenter, or else the nearest state that contains the source and every target. It enters its targets, their ancestors up to the domain and, of each parallel state among those ancestors or the domain, every region that holds no target, at its initial state. set.light is declared on the root, so its domain is the root: it leaves both regions, and entering mode again under the parallel root enters theme at its initial state. Stately’s page on transitions says such a transition leaves the other region as it is; XState 5.33.2 resets it, and xstate does what XState does. To move one region and keep the others, declare the transition in that region: mode.light, in mode, has mode as its domain, and theme keeps custom.

Self-transitions and reenter

A transition whose targets are inside its source, the source itself included, does not leave the source by default: the source’s exit and entry actions do not run, and the actors it invokes keep running. Only the states below the source are left and entered. With reenter: true the domain is above the source, so the source is left and entered again: its actions run, and its invoked actors are stopped and started again. A transition without a target leaves and enters no state, whatever its reenter says, as in XState.

A self-transition is one that stays in its source, and there are three kinds:

Transition The source’s children The source’s entry and exit actions The source’s invoked actors

targetless, no target

kept as they are

not run

keep running

targeted, the source as its target

left and entered at their initial states

not run

keep running

re-entering, a target and reenter: true

left and entered at their initial states

run

stopped and started again

The machine below names an entry and an exit action for each state. None has an implementation, so each is returned to the caller in the order it runs (Custom actions), and the output shows what each transition left and entered. types_of, a helper of the program, writes the types of a list of actions as [a, b]:

const boost::json::value config = boost::json::parse(R"({
    "id": "machine",
    "initial": "parentState",
    "states": {
        "parentState": {
            "entry": "enterParent",
            "exit": "exitParent",
            "initial": "someChildState",
            "on": {
                "event.targetless": {"actions": "notify"},
                "event.normal": {"target": ".someChildState"},
                "event.thatReenters": {"target": ".otherChildState", "reenter": true},
                "event.self": {"target": "parentState"},
                "event.selfReenters": {"target": "parentState", "reenter": true}
            },
            "states": {
                "someChildState": {"entry": "enterSome", "exit": "exitSome"},
                "otherChildState": {"entry": "enterOther", "exit": "exitOther"}
            }
        }
    }
})");
const xstate::result<xstate::machine> machine = xstate::create_machine(config, {});
if (!machine.has_value()) {
    return 1;
}

auto [now, actions] = xstate::initial_transition(*machine);
std::cout << "start " << boost::json::serialize(now.value) << ' ' << types_of(actions) << '\n';
for (const std::string_view type : {
         "event.thatReenters",
         "event.targetless",
         "event.self",
         "event.normal",
         "event.selfReenters",
     }) {
    const xstate::event happened{.type = std::string(type), .payload = {}};
    std::tie(now, actions) = xstate::transition(*machine, now, happened);
    std::cout << type << " -> " << boost::json::serialize(now.value) << ' ' << types_of(actions)
              << '\n';
}

It prints:

start {"parentState":"someChildState"} [enterParent, enterSome]
event.thatReenters -> {"parentState":"otherChildState"} [exitSome, exitParent, enterParent, enterOther]
event.targetless -> {"parentState":"otherChildState"} [notify]
event.self -> {"parentState":"someChildState"} [exitOther, enterSome]
event.normal -> {"parentState":"someChildState"} [exitSome, enterSome]
event.selfReenters -> {"parentState":"someChildState"} [exitSome, exitParent, enterParent, enterSome]
  • event.thatReenters goes from parentState to its child otherChildState, and re-enters: parentState is left and entered too.

  • event.targetless runs notify and nothing else; otherChildState stays.

  • event.self targets parentState itself: its child is left and its initial child entered, and the entry and exit actions of parentState itself do not run.

  • event.normal targets .someChildState, the active child: the child is inside the domain, so it is left and entered again.

  • event.selfReenters re-enters parentState and everything in it.

In XState v4 a re-entering transition was called external, and the default one internal.

Event descriptors

The key of an on entry is an event descriptor: it says which event types its transitions take. Most descriptors are a type and match only that type. Two forms match more. A wildcard is a catch-all, for the events a state does not name; a partial descriptor handles a family of events named with a common prefix, such as every feedback. event, in one place:

  • *, the wildcard, matches every event type, the machine’s own included;

  • a partial descriptor, a prefix and then .*, matches every type that starts with the prefix’s dot-separated tokens: feedback.* matches feedback, feedback.good and feedback.bad.rude, and not feedbacks.

* is a wildcard only as the whole descriptor, or as the last token after a dot when no earlier token is * itself. A * inside a longer token is a plain character: mou*se.* is a partial descriptor and matches mou*se.x. Any other descriptor that holds a *, such as mouse*, *.click, mouse.*.click or mouse.*.*, matches only an event of exactly that type. xstate writes no warning about any of them, where XState’s development build writes a console warning about a descriptor that ends in .* and holds another *, such as mouse.*.* or mou*se.*, the first time a state looks up the transitions of an event type (The checks of XState’s development build).

One event can match several descriptors of one state. Their transitions are tried in this order: those of the descriptor equal to the event’s type, then those of each matching wildcard descriptor, the longest first, so feedback.bad.* before feedback.* before *. The first enabled transition is taken, and only when none is does the search go up to the parent.

const boost::json::value config = boost::json::parse(R"({
    "id": "feedback",
    "initial": "prompt",
    "states": {
        "prompt": {
            "on": {
                "feedback.good": "thanks",
                "feedback.*": "form",
                "feedback.bad.*": "apology",
                "*": "other"
            }
        },
        "thanks": {},
        "form": {},
        "apology": {},
        "other": {}
    }
})");
const xstate::result<xstate::machine> feedback = xstate::create_machine(config, {});
if (!feedback.has_value()) {
    return 1;
}

const xstate::snapshot prompt = xstate::initial_transition(*feedback).first;
for (const std::string_view type : {
         "feedback.good",
         "feedback.bad",
         "feedback.bad.rude",
         "feedback.meh",
         "feedback",
         "feedbacks",
         "mouse.click",
         "*",
     }) {
    const xstate::event happened{.type = std::string(type), .payload = {}};
    const xstate::snapshot next = xstate::transition(*feedback, prompt, happened).first;
    if (next.status == xstate::status::error) {
        std::cout << type << " -> " << next.error.message() << '\n';
        continue;
    }
    std::cout << type << " -> " << boost::json::serialize(next.value) << '\n';
}

It prints:

feedback.good -> "thanks"
feedback.bad -> "apology"
feedback.bad.rude -> "apology"
feedback.meh -> "form"
feedback -> "form"
feedbacks -> "other"
mouse.click -> "other"
* -> invalid_event

feedback.bad is matched by feedback.bad.*, which is longer than feedback.*; feedbacks is not the token feedback, so only * matches it. Since the wildcard matches the machine’s own events too, a * transition in an active child state takes the xstate.after... event of its parent’s delayed transition before the parent sees it: the deepest state is tried first.

An event of the type *

An event may not have the type * when it comes from outside the machine. The macrostep that takes one settles at once with the snapshot it began from, its status error and its error invalid_event, which is the last line the example prints. XState’s development build throws there, An event cannot have the wildcard type ('*'), and xstate returns the error as a value (The checks of XState’s development build).

A raised event of the type * that the machine queues itself, a raise without a delay, is not refused: its type is the descriptor * exactly, so * transitions take it, and no partial descriptor matches it, as in XState. A delayed one comes back from the clock as a macrostep of its own and is refused like an event from outside, and an actor sent one, or receiving its own delayed one, fails with invalid_event, as XState’s actor ends in error.

Guards

A guard is a condition a transition checks before it is taken: a guarded transition is enabled only when its guard passes. Guards let one event lead to different states depending on data: the event’s payload, the context, or the params the config gives the guard. A guard decides and changes nothing.

In XState a guard is a function, inline in the config or named in setup({ guards }). A JSON config cannot hold a function, so in xstate every guard is a name, and implementations.guards maps each name to a predicate: a function of the action_args every implementation receives, the context, the event and the params, that returns a result<bool>. A guard is written as its name, or as an object with a type, its name, and params, a JSON value that reaches the guard as args.params, null when the config gives none; guards with the same name share one implementation, and their params tell one use from another. XState’s params may also be a function of the context and the event, which JSON cannot hold: an xstate guard reads both from its args.

When one event has a list of transitions, they are tried in order, and the first enabled one is taken; a last transition without a guard is the default, taken when no guard before it passes. integer_at, a helper of the program, returns the integer a JSON object holds under a key, or none:

xstate::implementations implementations;
implementations.guards.emplace(
    "sentimentGood", [](const xstate::action_args& args) -> xstate::result<bool> {
        const std::optional<std::int64_t> rating = integer_at(args.event.payload, "rating");
        return rating.has_value() && *rating >= 4;
    });
implementations.guards.emplace(
    "sentimentBad", [](const xstate::action_args& args) -> xstate::result<bool> {
        const std::optional<std::int64_t> rating = integer_at(args.event.payload, "rating");
        return rating.has_value() && *rating <= 2;
    });
implementations.guards.emplace(
    "isValid", [](const xstate::action_args& args) -> xstate::result<bool> {
        const boost::json::object* params = args.params.if_object();
        const std::optional<std::int64_t> max_length =
            params == nullptr ? std::nullopt : integer_at(*params, "maxLength");
        const boost::json::value* feedback = args.event.payload.if_contains("feedback");
        if (!max_length.has_value() || feedback == nullptr || !feedback->is_string()) {
            return xstate::failure<bool>(xstate::errc::implementation_failed);
        }
        const std::size_t length = feedback->get_string().size();
        return length > 0 && std::cmp_less_equal(length, *max_length);
    });
const boost::json::value config = boost::json::parse(R"({
    "id": "feedback",
    "initial": "prompt",
    "states": {
        "prompt": {
            "on": {
                "feedback.provide": [
                    {"guard": "sentimentGood", "target": "thanks"},
                    {"guard": "sentimentBad", "target": "form"},
                    {"target": "neutral"}
                ]
            }
        },
        "form": {
            "on": {
                "submit": {
                    "guard": {"type": "isValid", "params": {"maxLength": 50}},
                    "target": "submitting"
                }
            }
        },
        "thanks": {},
        "neutral": {},
        "submitting": {}
    }
})");
const xstate::result<xstate::machine> feedback =
    xstate::create_machine(config, implementations);
if (!feedback.has_value()) {
    return 1;
}
const xstate::snapshot prompt = xstate::initial_transition(*feedback).first;
for (const std::int64_t rating : {5, 1, 3}) {
    const xstate::event provide{.type = "feedback.provide", .payload = {{"rating", rating}}};
    const xstate::snapshot next = xstate::transition(*feedback, prompt, provide).first;
    std::cout << "rating " << rating << " -> " << boost::json::serialize(next.value) << '\n';
}

const xstate::event bad{.type = "feedback.provide", .payload = {{"rating", 1}}};
const xstate::snapshot form = xstate::transition(*feedback, prompt, bad).first;
for (const std::string_view text : {"", "Too slow."}) {
    const xstate::event submit{.type = "submit", .payload = {{"feedback", text}}};
    const xstate::snapshot next = xstate::transition(*feedback, form, submit).first;
    std::cout << "feedback " << boost::json::serialize(boost::json::value(text)) << " -> "
              << boost::json::serialize(next.value) << '\n';
}

const xstate::event no_feedback{.type = "submit", .payload = {}};
const xstate::snapshot failed = xstate::transition(*feedback, form, no_feedback).first;
std::cout << "no feedback -> " << boost::json::serialize(failed.value);
if (failed.status == xstate::status::error) {
    std::cout << " error " << failed.error.message();
}
std::cout << '\n';

It prints:

rating 5 -> "thanks"
rating 1 -> "form"
rating 3 -> "neutral"
feedback "" -> "form"
feedback "Too slow." -> "submitting"
no feedback -> "form" error implementation_failed

A rating of 3 passes neither guard, so the default is taken. In form, an empty feedback does not pass isValid: no transition is enabled and the machine stays. A guard whose input is wrong, here a submit without feedback, returns an error rather than a guess, and the macrostep fails with it (When a guard cannot run): the last line is the snapshot it began from, form, its status error and its error implementation_failed. XState’s transition throws there instead, Unable to evaluate guard 'isValid' ..., followed by the message of the TypeError its guard raised (Failures are values).

Higher-order guards and stateIn

XState composes guards with and, or and not, and tests the machine’s state with stateIn. All four are functions in XState, which JSON cannot hold, so xstate writes each as an object whose type names it:

{"type": "xstate.and", "guards": ["isValid", "isAuthorized"]}
{"type": "xstate.or", "guards": ["isAuthorized", "isGuest"]}
{"type": "xstate.not", "guards": ["isGuest"]}
{"type": "xstate.stateIn", "stateValue": {"form": "submitting"}}

An operand in guards is a guard in any form: a name, an object with params, or another higher-order guard. and passes when every operand passes, and or when one does; like XState’s, both stop at the first operand that decides, so an operand after it is not evaluated. not takes exactly one operand. A not with any other number of operands, or an and or or without a guards list, is refused with invalid_config.

stateIn passes when the snapshot is in stateValue: a state value, matched as snapshot.matches matches it, or a string that starts with #, which passes when the state with that id is active. It tests the whole machine’s state, not the source’s, which is mostly useful in a parallel state, where one region depends on another. In the editor below, save needs the network region online and the role of an owner or an admin, and edit refuses a guest:

xstate::implementations implementations;
implementations.guards.emplace(
    "hasRole", [](const xstate::action_args& args) -> xstate::result<bool> {
        const boost::json::object* params = args.params.if_object();
        const boost::json::value* wanted =
            params == nullptr ? nullptr : params->if_contains("role");
        if (wanted == nullptr || !wanted->is_string()) {
            return xstate::failure<bool>(xstate::errc::implementation_failed);
        }
        const boost::json::value* role = args.event.payload.if_contains("role");
        return role != nullptr && *role == *wanted;
    });
const boost::json::value config = boost::json::parse(R"({
    "id": "editor",
    "type": "parallel",
    "states": {
        "document": {
            "initial": "dirty",
            "states": {
                "dirty": {
                    "on": {
                        "save": {
                            "guard": {
                                "type": "xstate.and",
                                "guards": [
                                    {"type": "xstate.stateIn", "stateValue": {"network": "online"}},
                                    {
                                        "type": "xstate.or",
                                        "guards": [
                                            {"type": "hasRole", "params": {"role": "owner"}},
                                            {"type": "hasRole", "params": {"role": "admin"}}
                                        ]
                                    }
                                ]
                            },
                            "target": "saved"
                        }
                    }
                },
                "saved": {
                    "on": {
                        "edit": {
                            "guard": {
                                "type": "xstate.not",
                                "guards": [{"type": "hasRole", "params": {"role": "guest"}}]
                            },
                            "target": "dirty"
                        }
                    }
                }
            }
        },
        "network": {
            "initial": "offline",
            "states": {
                "offline": {"on": {"connect": "online"}},
                "online": {}
            }
        }
    }
})");
const xstate::result<xstate::machine> editor = xstate::create_machine(config, implementations);
if (!editor.has_value()) {
    return 1;
}
const std::vector<std::pair<std::string_view, std::string_view>> steps{
    {"save", "owner"}, {"connect", ""},   {"save", "guest"},
    {"save", "admin"}, {"edit", "guest"}, {"edit", "owner"},
};
xstate::snapshot now = xstate::initial_transition(*editor).first;
for (const auto& [type, role] : steps) {
    xstate::event happened{.type = std::string(type), .payload = {}};
    std::cout << type;
    if (!role.empty()) {
        happened.payload.emplace("role", role);
        std::cout << ' ' << role;
    }
    now = xstate::transition(*editor, now, happened).first;
    std::cout << " -> " << boost::json::serialize(now.value) << '\n';
}

It prints:

save owner -> {"document":"dirty","network":"offline"}
connect -> {"document":"dirty","network":"online"}
save guest -> {"document":"dirty","network":"online"}
save admin -> {"document":"saved","network":"online"}
edit guest -> {"document":"saved","network":"online"}
edit owner -> {"document":"dirty","network":"online"}

to_json writes these guards in the form above, so a machine loaded back from its definition keeps them, where XState’s machine.toJSON() writes nothing in their place, since they are functions (Higher-order guards).

When a guard cannot run
  • A guard name the implementations do not hold is refused when the machine is created, with unknown_guard. XState accepts the machine and throws when it first evaluates the guard; xstate refuses a name it cannot resolve at creation (A config is refused when it is created).

  • A guard that returns an error fails the macrostep: it settles at once, with the snapshot it began from, its status error and its error the one the guard returned. XState’s pure functions throw there, and its actor keeps the snapshot from before the event, in error, as xstate’s does.

  • A stateIn whose #id names no state is accepted when the machine is created; evaluating it fails the macrostep with unknown_state, where XState’s getStateNodeById throws. A state value that names no state does not pass.

Eventless transitions

An eventless transition is taken without an event: whenever its state is active and its guard passes, right after the microstep that made it so. It is declared under a state’s always, in any of the forms of a transition in on. It fits a change that follows from a condition rather than from a trigger: the kettle below boils when its temperature, which events update, goes above 100, whatever event raised it.

updateTemperature is an assign that copies the event’s temperature into the context (Updating context with assign); turnOffLight has no implementation, so it is a custom action, returned to the caller. isBoiling reads the context with integer_at, the helper of the quick start (Handle context data), which returns the integer an object holds under a key, or none. The example prints, for each event, every microstep of its macrostep, with the state value it left and the actions it returned, written by the types_of of Self-transitions and reenter:

xstate::implementations implementations;
implementations.guards.emplace(
    "isBoiling", [](const xstate::action_args& args) -> xstate::result<bool> {
        const std::optional<std::int64_t> temperature = integer_at(args.context, "temperature");
        if (!temperature.has_value()) {
            return xstate::failure<bool>(xstate::errc::implementation_failed);
        }
        return *temperature > 100;
    });
implementations.actions.emplace(
    "updateTemperature",
    xstate::assign_action{
        .assignment =
            [](const xstate::action_args& args) -> xstate::result<boost::json::object> {
            const boost::json::value* temperature =
                args.event.payload.if_contains("temperature");
            if (temperature == nullptr) {
                return xstate::failure<boost::json::object>(
                    xstate::errc::implementation_failed);
            }
            return boost::json::object{{"temperature", *temperature}};
        },
    });
const boost::json::value config = boost::json::parse(R"({
    "id": "kettle",
    "initial": "lukewarm",
    "context": {"temperature": 80},
    "on": {"temp.update": {"actions": "updateTemperature"}},
    "states": {
        "lukewarm": {"on": {"boil": "heating"}},
        "heating": {"always": {"guard": "isBoiling", "target": "boiling"}},
        "boiling": {
            "entry": "turnOffLight",
            "always": {
                "guard": {"type": "xstate.not", "guards": ["isBoiling"]},
                "target": "heating"
            }
        }
    }
})");
const xstate::result<xstate::machine> kettle = xstate::create_machine(config, implementations);
if (!kettle.has_value()) {
    return 1;
}
const std::vector<std::pair<std::string_view, boost::json::object>> events{
    {"temp.update", {{"temperature", 105}}},
    {"boil", {}},
    {"temp.update", {{"temperature", 90}}},
};
xstate::snapshot now = xstate::initial_transition(*kettle).first;
for (const auto& [type, payload] : events) {
    const xstate::event happened{.type = std::string(type), .payload = payload};
    std::cout << type << ':';
    for (const xstate::microstep& micro : xstate::get_microsteps(*kettle, now, happened)) {
        std::cout << ' ' << boost::json::serialize(micro.snapshot.value) << ' '
                  << types_of(micro.actions);
        now = micro.snapshot;
    }
    std::cout << '\n';
}

It prints:

temp.update: "lukewarm" []
boil: "heating" [] "boiling" [turnOffLight]
temp.update: "boiling" [] "heating" []

The first update raises the temperature to 105 while the kettle is lukewarm, which has no eventless transition: one microstep. boil takes two: its own transition to heating, and then, since the temperature is already above 100, the eventless transition of heating to boiling. The macrostep settles in boiling, so heating is a transient state there: the machine passes through it and never rests in it. The last update is a targetless transition of the root. Its microstep lowers the temperature in the context, and the eventless transitions, selected after every event’s microstep, now find that the guard of boiling passes.

The rules, which are XState’s:

  • To select the eventless transitions, the machine walks up from each active atomic state and takes the first eventless transition whose guard passes. Those it finds run as one microstep, before the next raised event is taken.

  • It selects them after the macrostep’s first microstep, the event’s or the initial one, and after each raised event’s. After a microstep of eventless transitions, it selects them again only when that microstep changed the snapshot: the active states, the history, the context (an assign ran), the children (a spawn or a stop) or the status. XState compares snapshot objects; xstate states the rule. So an unguarded targetless always whose actions change nothing runs once per macrostep and once more after each raised event; one that assigns runs forever.

  • Their guards and actions receive the macrostep’s event: the external one, or the raised event the machine took last. Each microstep reports it as microstep.event.

  • A transient state is never a settled snapshot: the actor layer’s subscribers do not see it (Watching an actor), as XState’s do not. The cursor and get_microsteps report every microstep, so the machine core shows it. A delayed transition with a delay of 0 does not run inside the macrostep: its event comes back from the clock, so the macrostep settles in its state first (Delayed transitions).

A macrostep that never settles

An eventless transition that is always enabled and always changes the snapshot never lets its macrostep settle: two states whose always lead to each other, or an unguarded always that assigns. Stately advises a target that differs from the current state, or a guard that turns false. Stately’s page says an unguarded, targetless always always loops; in XState 5.33.2, as in xstate, it loops only when its actions change the snapshot.

xstate never stops such a macrostep on its own: the functions that run a macrostep to its end, transition and get_next_snapshot among them, never return. The cursor runs one microstep per next(), so its caller decides how many it pays for. Here it pays for five, and the two states of the cycle take turns:

const boost::json::value config = boost::json::parse(R"({
    "id": "loop",
    "initial": "idle",
    "states": {
        "idle": {"on": {"start": "ping"}},
        "ping": {"always": "pong"},
        "pong": {"always": "ping"}
    }
})");
const xstate::result<xstate::machine> loop = xstate::create_machine(config, {});
if (!loop.has_value()) {
    return 1;
}

const xstate::snapshot idle = xstate::get_initial_snapshot(*loop);
xstate::macrostep step = xstate::begin(*loop, idle, {.type = "start", .payload = {}});
std::size_t spent = 0;
for (std::size_t fuel = 5; fuel > 0 && !step.done(); --fuel) {
    const xstate::progress made = step.next();
    ++spent;
    std::cout << boost::json::serialize(made.step.snapshot.value) << '\n';
}
std::cout << (step.done() ? "settled" : "not settled") << " after " << spent << " microsteps\n";

It prints:

"ping"
"pong"
"ping"
"pong"
"ping"
not settled after 5 microsteps

The cursor is still valid when the fuel runs out, and a later next() resumes it where it stopped (Fuel). What a caller may do then is shown with the cursor (Out of fuel in a cycle).

XState bounds such a macrostep only through the config’s options.maxIterations: once that many microsteps have followed the event’s own with the machine still active, the macrostep throws, even when it would settle there, and without the option transition never returns. xstate does not read options, and the fuel of the caller that drives the cursor is the bound (Fuel).

Delayed transitions

A delayed transition is taken when its state has been active for a given time. It is declared under a state’s after, whose keys are delays and whose values are transitions in any of the forms of on. Leaving the state before the time has passed cancels it. Delayed transitions make timeouts and retries part of the machine.

A delay is written as a key of after:

  • a whole number of milliseconds, such as "5000";

  • the name of a delay of implementations.delays, a delay_maker: a function of the action_args that returns the milliseconds, XState’s setup({ delays }). It is computed each time the state is entered, from the context of that moment. XState’s delays also maps a name to a number; in xstate a constant named delay is a delay_maker that returns that number.

A name the implementations do not hold is refused when the machine is created, with unknown_delay; XState takes such a delay as no delay at all, and the transition is taken in the same macrostep (A config is refused when it is created). A key that is not all digits and starts with a digit, a sign or a dot, such as "1.5", "-1" or "1e3", is refused with invalid_config, because time in xstate is whole milliseconds written as digits; XState reads "1e3" and "+5" as numbers (Time in whole milliseconds). A key that starts with a space is a name to xstate, where XState reads " 5" as 5.

In the machine core

The machine core never reads a clock. Entering a state with after returns, for each delay, the action xstate.raise of the event xstate.after.<delay>.<state id>, <delay> being the number or the delay’s name, with its delay in milliseconds, computed for a named one, and with that type as its id; leaving the state returns xstate.cancel of that id. The state’s delayed transitions are transitions on that event.

Keeping time out of the machine makes a step a function of the snapshot and the event alone, so the same events give the same steps in a test and in production. Time is one more input, which the caller brings when it chooses: a caller that keeps a snapshot between two requests keeps its clock beside it, and moves the time when the next request comes.

In the game below, the initial macrostep returns the raise, and pushing the button the cancel, before logSuccess. print_actions writes the actions a step returns, each with its params:

/** Prints a macrostep's actions, one a line: its type, and its params when it has some. */
void print_actions(std::string_view macrostep, const std::vector<xstate::action>& actions) {
    std::cout << macrostep << ":\n";
    for (const xstate::action& one : actions) {
        std::cout << "  " << one.type;
        if (!one.params.is_null()) {
            std::cout << ' ' << boost::json::serialize(one.params);
        }
        std::cout << '\n';
    }
}
const boost::json::value config = boost::json::parse(R"({
    "id": "game",
    "initial": "waitingForButtonPush",
    "states": {
        "waitingForButtonPush": {
            "after": {
                "5000": {"target": "timedOut", "actions": "logThatYouGotTimedOut"}
            },
            "on": {
                "PUSH_BUTTON": {"actions": "logSuccess", "target": "success"}
            }
        },
        "success": {},
        "timedOut": {}
    }
})");
const xstate::result<xstate::machine> game = xstate::create_machine(config, {});
if (!game.has_value()) {
    return 1;
}
const auto [waiting, entered] = xstate::initial_transition(*game);
print_actions("initial macrostep", entered);
const xstate::event push{.type = "PUSH_BUTTON", .payload = {}};
print_actions("PUSH_BUTTON", xstate::transition(*game, waiting, push).second);

It prints:

initial macrostep:
  xstate.raise {"event":{"type":"xstate.after.5000.game.waitingForButtonPush"},"id":"xstate.after.5000.game.waitingForButtonPush","delay":5000}
PUSH_BUTTON:
  xstate.cancel {"sendId":"xstate.after.5000.game.waitingForButtonPush"}
  logSuccess

These are the actions, with the same params, that XState’s initialTransition and transition return.

Entering a state with several delays returns their raises in the order JavaScript enumerates the keys of after, as XState reads them: a key that is an array index, such as "500", first, by number, then the others as written. So with "1000" written before "500", the raise of 500 comes first.

A simulated_clock keeps the delayed events: XState’s SimulatedClock with its scheduler, as a value the caller owns. apply schedules each delayed raise and applies each cancel among the built-in actions a step returns; a custom action is the caller’s, whatever the config named it, and apply leaves it alone. set and increment move the time, never backwards: set to an earlier time is refused with invalid_config, where XState’s SimulatedClock throws (The core’s clock hands over the due events). pop_due takes out the earliest event whose time has come, by deadline and then in the order it was scheduled, and the caller delivers it as a macrostep of its own, whose actions go to the clock before the next pop_due. Due events come out one at a time, so an event scheduled while another is delivered is ordered as XState orders it, and one that a delivery cancels never comes out. Delayed events and the simulated clock lists the clock’s members. An actor does all of this itself (In the actor layer).

The same program goes on with two players. execute hands each action a step returns to the clock and runs the custom ones, which here print their name; advance moves the clock to a time and delivers each event that comes due by then:

/** Hands each action to the clock and runs the custom ones, which here print their name. */
void execute(const std::vector<xstate::action>& actions, xstate::simulated_clock& clock) {
    for (const xstate::action& one : actions) {
        clock.apply(one);
        if (!one.builtin) {
            std::cout << one.type << '\n';
        }
    }
}

/** Moves the clock to `time` and delivers each event that comes due, a macrostep each. */
xstate::result<xstate::snapshot> advance(const xstate::machine& machine, xstate::snapshot now,
                                         xstate::simulated_clock& clock, std::uint64_t time) {
    if (const xstate::result<void> moved = clock.set(time); !moved.has_value()) {
        return moved.error();
    }
    while (const std::optional<xstate::event> due = clock.pop_due()) {
        auto [next, actions] = xstate::transition(machine, now, *due);
        execute(actions, clock);
        now = std::move(next);
    }
    return now;
}
/** A player who never pushes. */
{
    xstate::simulated_clock clock;
    auto [now, actions] = xstate::initial_transition(*game);
    execute(actions, clock);
    std::cout << "at 0: " << boost::json::serialize(now.value) << '\n';
    for (const std::uint64_t time : {4999, 5000}) {
        const xstate::result<xstate::snapshot> later = advance(*game, now, clock, time);
        if (!later.has_value()) {
            return 1;
        }
        now = *later;
        std::cout << "at " << time << ": " << boost::json::serialize(now.value) << '\n';
    }
}

/** A player who pushes the button at 1000. */
{
    xstate::simulated_clock clock;
    auto [now, actions] = xstate::initial_transition(*game);
    execute(actions, clock);
    std::cout << "at 0: " << boost::json::serialize(now.value) << '\n';
    const xstate::result<xstate::snapshot> pushed = advance(*game, now, clock, 1000);
    if (!pushed.has_value()) {
        return 1;
    }
    std::tie(now, actions) =
        xstate::transition(*game, *pushed, {.type = "PUSH_BUTTON", .payload = {}});
    execute(actions, clock);
    std::cout << "at 1000: " << boost::json::serialize(now.value) << '\n';
    const xstate::result<xstate::snapshot> later = advance(*game, now, clock, 6000);
    if (!later.has_value()) {
        return 1;
    }
    std::cout << "at 6000: " << boost::json::serialize(later->value) << '\n';
}

It prints:

at 0: "waitingForButtonPush"
at 4999: "waitingForButtonPush"
logThatYouGotTimedOut
at 5000: "timedOut"
at 0: "waitingForButtonPush"
logSuccess
at 1000: "success"
at 6000: "success"

The first player’s timer comes due at 5000: advance delivers its event, the delayed transition is taken, and logThatYouGotTimedOut runs. The second player pushes at 1000; leaving waitingForButtonPush cancels the timer, so nothing comes due at 6000.

In the actor layer

An actor of actor_system arms a timer for each built-in delayed raise its machine returns, and removes it for each built-in cancel; a custom action goes to the host’s on_action whatever its name (Custom actions). Time is the host’s: clock_tick(now) moves the system’s time to now, in milliseconds from 0, and releases the timers due by then one at a time, in (deadline, arming) order, running the system after each, so a timer’s transition can cancel a later timer of the same tick, as XState’s SimulatedClock does. A stopped actor’s timers never fire. clock_tick does not refuse a time before the last one: it sets the time back, and a timer armed afterwards counts from it, where the core’s simulated_clock refuses to go back and XState’s SimulatedClock throws (A tick earlier than the last). Pass a time that never decreases.

The machine below retries with a delay that grows with each attempt, and gives up after the third. The delay timeout is computed from the context each time attempting is entered. It, the guard and the assign read the context with the kettle’s integer_at (Eventless transitions):

xstate::implementations implementations;
implementations.delays.emplace(
    "timeout", [](const xstate::action_args& args) -> xstate::result<std::uint64_t> {
        const std::optional<std::int64_t> attempts = integer_at(args.context, "attempts");
        if (!attempts.has_value() || *attempts < 0) {
            return xstate::failure<std::uint64_t>(xstate::errc::implementation_failed);
        }
        return static_cast<std::uint64_t>(*attempts) * 1000;
    });
implementations.guards.emplace(
    "canRetry", [](const xstate::action_args& args) -> xstate::result<bool> {
        const std::optional<std::int64_t> attempts = integer_at(args.context, "attempts");
        if (!attempts.has_value()) {
            return xstate::failure<bool>(xstate::errc::implementation_failed);
        }
        return *attempts < 3;
    });
implementations.actions.emplace(
    "countAttempt",
    xstate::assign_action{
        .assignment =
            [](const xstate::action_args& args) -> xstate::result<boost::json::object> {
            const std::optional<std::int64_t> attempts = integer_at(args.context, "attempts");
            if (!attempts.has_value()) {
                return xstate::failure<boost::json::object>(
                    xstate::errc::implementation_failed);
            }
            return boost::json::object{{"attempts", *attempts + 1}};
        },
    });
const boost::json::value config = boost::json::parse(R"({
    "id": "retry",
    "initial": "attempting",
    "context": {"attempts": 1},
    "states": {
        "attempting": {
            "after": {
                "timeout": [
                    {
                        "guard": "canRetry",
                        "actions": "countAttempt",
                        "target": "attempting",
                        "reenter": true
                    },
                    {"target": "failed"}
                ]
            }
        },
        "failed": {}
    }
})");
const xstate::result<xstate::machine> retry = xstate::create_machine(config, implementations);
if (!retry.has_value()) {
    return 1;
}
xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> actor = system.create_actor(*retry);
if (!actor.has_value() || !system.start(*actor).has_value()) {
    return 1;
}
for (const std::uint64_t now : {0, 1000, 2999, 3000, 6000}) {
    if (!system.clock_tick(now).has_value()) {
        return 1;
    }
    const xstate::snapshot* state = system.snapshot_of(*actor);
    if (state == nullptr) {
        return 1;
    }
    std::cout << "at " << now << ": " << boost::json::serialize(state->value) << ' '
              << boost::json::serialize(state->context) << '\n';
}

It prints:

at 0: "attempting" {"attempts":1}
at 1000: "attempting" {"attempts":2}
at 2999: "attempting" {"attempts":2}
at 3000: "attempting" {"attempts":3}
at 6000: "failed" {"attempts":3}

The timer armed at 0 is due at 1000. Its transition counts the attempt and re-enters attempting: the exit cancels the timer that fired, and the entry arms a new one with the delay of the new context, 2000 milliseconds, due at 3000, so nothing happens at 2999. At 6000 canRetry does not pass, and the second transition, the default, is taken. Without reenter: true the transition would not leave attempting, its entry would not run again, and no timer would be armed after the first: Stately’s example of a dynamic delay omits it, and so retries once.

In XState an actor takes its clock from its options, a SimulatedClock in tests and the real timers otherwise. The actor layer reads no clock at all: clock_tick is how time passes, in tests and in production (Delayed events and time).

Actions

An action is an effect a machine causes as it moves from state to state: it updates the context, sends an event, writes a log line, or asks your program to do something. Actions are fire-and-forget: the machine does not wait for one to finish. A machine runs actions in three places: on a transition, listed in its actions; when it enters a state, listed in the state’s entry; and when it leaves a state, listed in the state’s exit.

In XState an action is a function, and setup({ actions }) gives a name to the functions a config refers to. A JSON config cannot hold a function, so in xstate every action a config writes is a name, with optional params, and the implementations given to create_machine decide what each name is:

  • A name the implementations hold is a built-in action: one of XState’s action creators, assign, raise, sendTo, log, emit and the others, registered as a value that says what it does. The library resolves it.

  • Any other name is a custom action: an effect of your program. The machine core returns it to its caller, in order, as XState’s transition() returns its actions; the actor layer hands it to the callback your program gives on_action.

The split keeps the machine core pure. Resolving a microstep computes the next snapshot and the list of effects, and performs no effect outside the snapshot: your program performs the custom ones, and the actor layer, or your program when it calls the core itself, the built-in ones that reach other actors, timers and listeners.

Entry, exit and transition actions

Entry actions run on every transition that enters their state, exit actions on every transition that leaves it, and a transition’s actions on that transition. Each of entry, exit and a transition’s actions holds one action or a list of them. create_machine refuses a null one with invalid_config; XState reads it as a list that holds null, and fails when it runs it. A transition with no target runs its actions and nothing else: it leaves and enters no state, even with reenter: true. A transition whose target is its source or one of the source’s descendants does not leave the source either, and the source’s exit and entry actions run only when it sets reenter: true (Self-transitions and reenter).

The machine below lists a custom action at each place: exitAction when it leaves question, track on the transition, showConfetti when it enters thanks. None has an implementation, so each one is returned:

const boost::json::value config = boost::json::parse(R"({
    "id": "feedback",
    "initial": "question",
    "states": {
        "question": {
            "exit": "exitAction",
            "on": {
                "feedback.good": {"target": "thanks", "actions": "track"}
            }
        },
        "thanks": {"entry": "showConfetti"}
    }
})");
const xstate::result<xstate::machine> feedback = xstate::create_machine(config, {});
if (!feedback.has_value()) {
    return 1;
}

const xstate::snapshot question = xstate::get_initial_snapshot(*feedback);
const auto [thanks, actions] =
    xstate::transition(*feedback, question, {.type = "feedback.good", .payload = {}});
std::cout << "feedback.good:";
for (const xstate::action& one : actions) {
    std::cout << ' ' << one.type;
}
std::cout << '\n';

It prints:

feedback.good: exitAction track showConfetti

The machine left question, took the transition, and entered thanks: the exit action first, then the transition’s, then the entry action.

A machine that only reacts to events, with no states, is written with transitions that have actions and no target (Assign is one; Self-transitions and reenter has the rules).

Nested states and the root

A transition may leave and enter several states at once: a state and its ancestors, up to the root, which is a state too. The machine below nests asking in question, gives the root entry and exit actions, and ends in a final state:

const boost::json::value config = boost::json::parse(R"({
    "id": "feedback",
    "initial": "question",
    "entry": "rootEntry",
    "exit": "rootExit",
    "states": {
        "question": {
            "initial": "asking",
            "entry": "enterQuestion",
            "exit": "exitQuestion",
            "states": {
                "asking": {
                    "entry": "enterAsking",
                    "exit": "exitAsking",
                    "on": {
                        "feedback.good": {"target": "#feedback.thanks", "actions": "track"}
                    }
                }
            }
        },
        "thanks": {"type": "final", "entry": "showConfetti", "exit": "exitThanks"}
    }
})");
const xstate::result<xstate::machine> feedback = xstate::create_machine(config, {});
if (!feedback.has_value()) {
    return 1;
}

const auto [initial, initial_actions] = xstate::initial_transition(*feedback);
std::cout << "initial:";
for (const xstate::action& one : initial_actions) {
    std::cout << ' ' << one.type;
}
std::cout << '\n';

const auto [next, actions] =
    xstate::transition(*feedback, initial, {.type = "feedback.good", .payload = {}});
std::cout << "feedback.good:";
for (const xstate::action& one : actions) {
    std::cout << ' ' << one.type;
}
std::cout << '\n';

It prints:

initial: rootEntry enterQuestion enterAsking
feedback.good: exitAsking exitQuestion track showConfetti exitThanks rootExit

The order is XState’s, and it is fixed within a microstep. It follows the document order of the states: a parent before its children, and siblings in the order XState reads the keys of states. That is the order JavaScript enumerates an object’s keys in: a key that is an array index, such as "0" or "2", first, by number, then the rest as written. A microstep runs:

  1. the exit actions of the states it leaves, in reverse document order, so a child before its parent; each state’s exit actions are followed by an xstate.stopChild for each child it invokes;

  2. the actions of the transitions it takes, each transition’s in the order listed. When parallel regions take several, the transitions an event selects come in the order of the regions in the state value, whose keys are in the order JavaScript enumerates them, and eventless ones in the order of the active atomic states in nodes (The state value);

  3. the entry actions of the states it enters, in document order, so a parent before its children; each state’s entry actions are followed by the spawns of the children it invokes, then, when it is entered by default and its initial is an object with actions, those actions. A compound state is entered by default when no target of the transition is inside it, and it then enters its initial child.

Document order is not depth. When a parallel state is left and entered again, every state of its second region is left before any of its first, and every state of its first region is entered before any of its second.

The root’s entry actions run in the machine’s first microstep, before those of the states it enters. When a transition reaches a final state of the root, the machine is done, and in that same microstep the exit actions of every state still active run, in reverse document order, the root’s last: that is why exitThanks and rootExit follow showConfetti. Stately’s documentation says a top-level final state cannot have exit actions; XState 5.33.2 runs them when the machine completes, and xstate does the same.

Action objects and params

An action in a config is a string, its name, or an object with a type, its name, and optional params, a JSON value for that use of it. Actions with the same type share one implementation; their params tell one use from another. Here the transition lists two actions with params: track, which has no implementation, and addRating, an assign that adds its param by to the rating:

const xstate::assigner add_rating =
    [](const xstate::action_args& args) -> xstate::result<boost::json::object> {
    const boost::json::object* params = args.params.if_object();
    const boost::json::object* context = args.context.if_object();
    const boost::json::value* by = params == nullptr ? nullptr : params->if_contains("by");
    const boost::json::value* rating =
        context == nullptr ? nullptr : context->if_contains("rating");
    if (by == nullptr || !by->is_int64() || rating == nullptr || !rating->is_int64()) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"rating", rating->get_int64() + by->get_int64()}};
};
xstate::implementations implementations;
implementations.actions.emplace("addRating", xstate::assign_action{.assignment = add_rating});

const boost::json::value config = boost::json::parse(R"({
    "id": "feedback",
    "initial": "question",
    "context": {"rating": 0},
    "states": {
        "question": {
            "on": {
                "feedback.good": {
                    "target": "thanks",
                    "actions": [
                        {"type": "track", "params": {"response": "good"}},
                        {"type": "addRating", "params": {"by": 5}}
                    ]
                }
            }
        },
        "thanks": {"entry": "showConfetti"}
    }
})");
const xstate::result<xstate::machine> feedback =
    xstate::create_machine(config, implementations);
if (!feedback.has_value()) {
    return 1;
}

const xstate::snapshot question = xstate::get_initial_snapshot(*feedback);
const auto [thanks, actions] =
    xstate::transition(*feedback, question, {.type = "feedback.good", .payload = {}});
for (const xstate::action& one : actions) {
    std::cout << one.type;
    if (!one.params.is_null()) {
        std::cout << ' ' << boost::json::serialize(one.params);
    }
    std::cout << '\n';
}
std::cout << "context " << boost::json::serialize(thanks.context) << '\n';

It prints:

track {"response":"good"}
showConfetti
context {"rating":5}

track has no implementation: it is a custom action, returned with the params the config gave it. addRating is a built-in, an assign_action (Built-in and custom actions, Assign), so it changed the context and was not returned; its implementation read its params from args.params. showConfetti, written as a string, has no params, and its params is null.

Every action implementation, and every guard, delay and value maker, receives an action_args, which XState hands an action as ({ context, event }, params):

context

The context as it is when the action runs: an assign earlier in the same microstep has already changed it.

event

The microstep’s event.

params

The params the config gave the action or the guard, null when it gives none.

XState also takes params as a function of the context and the event, computed when the action runs. JSON holds no function, so xstate’s params are the fixed value the config writes. An implementation that needs the context or the event reads them from its action_args. A caller executing a custom action finds the event in the microstep that returned it, and the context as that whole microstep left it, after any assign listed after the action (How guards and actions read the context). A value that must be computed at the action’s turn is computed by a registered implementation (Actions).

Built-in and custom actions

Each built-in action is one alternative of action_implementation, registered under the name the config uses, with XState’s arguments as its members. The machine core resolves it as XState resolves the action creator and returns it as XState’s transition() returns it: an action of the same type xstate.*, whose params are XState’s less what is not JSON or has no value. An actor reference is left out, and a targetId or an id names the actor instead; an id, a delay or a label with no value is left out.

There are two exceptions, both sends whose receiver depends on where the machine runs. XState’s pure functions run the machine as an inert actor, which has no parent and a fresh system of its own; xstate’s core returns the send for its caller to resolve:

  • On a send or a forward to the parent, a sendParent or a target #_parent, XState’s transition() throws; xstate returns an xstate.sendTo to #_parent (sendParent without a parent).

  • On a send or a forward to a systemId, which XState names with ({ system }) => system.get('notifier'), the inert actor’s system knows only the children the call itself spawns. For any other systemId, even one a child of the snapshot holds, a forwardTo throws in XState’s development build (The checks of XState’s development build), and a sendTo goes to the inert actor itself, with no targetId. xstate returns an xstate.sendTo whose targetId is #system:notifier (Targets that name actors).

Each built-in, what the core returns for it, and what the actor layer then does:

XState Registered as Returned to the caller Run by the actor layer

assign(...)

assign_action

never: the context changes

-

raise(event)

raise_action

xstate.raise {event}, the event already queued

nothing more

raise(event, { id, delay })

raise_action with a delay

xstate.raise {event, id, delay}

arms a timer

cancel(id)

cancel_action

xstate.cancel {sendId}

cancels the timer

sendTo(target, event, { id, delay })

send_to_action

xstate.sendTo {targetId, event, id, delay}

delivers the event once the macrostep has settled, or arms a timer

sendParent(event, { id, delay })

send_parent_action

xstate.sendTo to #_parent

as a sendTo

forwardTo(target, { id, delay })

forward_to_action

xstate.sendTo of the microstep’s event

as a sendTo

log(value, label)

log_action

xstate.log {value, label}

hands it to on_log

emit(event)

emit_action

xstate.emit {event}

hands the event to the actor’s listeners once the macrostep has settled

spawnChild(src, { id, systemId, input })

spawn_child_action

xstate.spawnChild {id, src, input, systemId}

creates the child (Spawning actors)

stopChild(id)

stop_child_action

xstate.stopChild {id}

stops the child (Stopping a child)

a function of your program

nothing

{type, params} as the config names it

hands it to on_action

The name decides, not the type a config writes. A config may name an action xstate.raise; if the implementations hold no such name, it is a custom action, returned with that type, as XState returns it. action::builtin tells the two apart. In an entry or an exit, an action named xstate.raise or xstate.cancel does not survive the machine’s definition: create_machine_from_definition cannot tell it from the raise and cancel an after adds, and drops it (The definition).

XState’s documentation warns that its built-in action creators are not imperative: calling assign(...) inside a custom action does nothing. In xstate a custom action runs in your program, outside the machine: it cannot change the context or send an event, and constructing an xstate::assign_action{...} there does nothing. Only a built-in registered under a name the config uses acts on the machine.

Every implementation returns a result. An error is the implementation’s failure, where XState’s would throw: the macrostep settles at once with the snapshot it began from, its status error and its error the code the implementation returned (When an implementation fails).

xstate has no inline action function and no params function, because JSON holds no function (Functions become names). It has no enqueueActions yet (What is not ported lists what xstate does not have yet). It has no machine.provide: create_machine plays that role, and the same config created with other implementations is another machine (A machine is immutable).

Custom actions

A custom action is how a machine asks your program to do something: send an email, write to a database, show confetti. The machine names it and your program implements it. In XState the implementation is a function of setup({ actions }); in xstate it is code of yours that runs the action the machine returns, here a function execute that reads the type and the params:

/** Runs a custom action the machine returned; one it does not know does nothing. */
void execute(const xstate::action& custom) {
    if (custom.type == "track") {
        const boost::json::object* params = custom.params.if_object();
        const boost::json::value* response =
            params == nullptr ? nullptr : params->if_contains("response");
        std::cout << "tracking " << (response == nullptr ? "-" : boost::json::serialize(*response))
                  << '\n';
        return;
    }
    if (custom.type == "showConfetti") {
        std::cout << "confetti!\n";
    }
}

const boost::json::value config = boost::json::parse(R"({
    "id": "feedback",
    "initial": "question",
    "states": {
        "question": {
            "on": {
                "feedback.good": {
                    "target": "thanks",
                    "actions": [
                        {"type": "track", "params": {"response": "good"}}
                    ]
                }
            },
            "exit": [{"type": "exitAction"}]
        },
        "thanks": {"entry": [{"type": "showConfetti"}]}
    }
})");
const xstate::result<xstate::machine> feedback = xstate::create_machine(config, {});
if (!feedback.has_value()) {
    return 1;
}
const xstate::event good{.type = "feedback.good", .payload = {}};

With the pure functions, transition returns the actions and the caller runs them, in order. Each carries its name and its params, where XState’s returned actions carry the function to run as exec:

const xstate::snapshot question = xstate::get_initial_snapshot(*feedback);
const auto [thanks, actions] = xstate::transition(*feedback, question, good);
std::cout << "returned:";
for (const xstate::action& one : actions) {
    std::cout << ' ' << one.type;
}
std::cout << '\n';
for (const xstate::action& one : actions) {
    execute(one);
}

An actor runs its custom actions while its macrostep is resolved, as XState’s actor runs them: the actor layer hands each to the callback given to on_action, here one that calls execute:

xstate::actor_system system({.fuel = 10'000});
const xstate::result<void> acting =
    system.on_action([](xstate::actor_ref, const xstate::action& custom) { execute(custom); });
if (!acting.has_value()) {
    return 1;
}
const xstate::result<xstate::actor_ref> actor = system.create_actor(*feedback);
if (!actor.has_value() || !system.start(*actor).has_value()) {
    return 1;
}
std::cout << "the actor:\n";
if (!system.send(*actor, good).has_value()) {
    return 1;
}

It prints:

returned: exitAction track showConfetti
tracking "good"
confetti!
the actor:
tracking "good"
confetti!

execute did nothing for exitAction: it ignores a type it does not know, as XState’s actor does nothing for an action that has no implementation.

  • The core returns a macrostep’s actions in order: transition and initial_transition return all of them, and the cursor returns each microstep’s in its microstep::actions, which macrostep_result::actions gathers. Run each action once: either each microstep’s as it comes, or the result’s at the end (Executing actions as they come has the details).

  • The actor layer hands each custom action to on_action as its microstep is resolved, with the actor that returned it. That is before the macrostep’s sends and emitted events, which wait until it has settled (Emitting events shows the order). An actor’s initial custom actions reach on_action when the actor starts, as XState runs them on start().

  • When a macrostep fails, the custom actions it resolved before the failure have already reached on_action, as XState’s actor has run them. The initial macrostep is the exception: an actor whose initial macrostep fails hands over none of its custom actions, as XState’s start() runs none for an actor born failed.

  • on_action holds one callback for the whole system, and a second call replaces the first. The callback runs inside the actor’s turn, while the system runs: it may call the members that read the system, but a call from it to one that changes the system, a send or a resolve among them, returns xactor’s invalid_argument and changes nothing. XState accepts a send made from an action’s function: one to the actor running the action is handled once the event in progress is done, and one to another actor that is idle is handled at once, before the function returns (A callback that calls the system back, Watching an actor).

  • An actor’s custom action reaches on_action with its type and the params the config wrote, nothing more: no context and no event, which XState hands an action’s function in its first argument, info. What must be computed from the context or the event at the action’s turn is computed by a registered built-in, an emit_action whose event_maker builds the event the host’s listener reads, or a log_action (Emitting events, Log).

Assign

assign updates the context, and it is the only action that does: the context is never written in place. Its implementation is an assigner, a function of action_args that returns the members to change as an object. Here addValue adds the event’s value to the count, and an actor’s listener prints each context the actor settles in:

const xstate::assigner add_value =
    [](const xstate::action_args& args) -> xstate::result<boost::json::object> {
    const boost::json::object* context = args.context.if_object();
    const boost::json::value* count =
        context == nullptr ? nullptr : context->if_contains("count");
    const boost::json::value* value = args.event.payload.if_contains("value");
    if (count == nullptr || !count->is_int64() || value == nullptr || !value->is_int64()) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"count", count->get_int64() + value->get_int64()}};
};
xstate::implementations implementations;
implementations.actions.emplace("addValue", xstate::assign_action{.assignment = add_value});

const boost::json::value config = boost::json::parse(R"({
    "context": {"count": 0, "unit": "clicks"},
    "on": {"increment": {"actions": "addValue"}}
})");
const xstate::result<xstate::machine> count_machine =
    xstate::create_machine(config, implementations);
if (!count_machine.has_value()) {
    return 1;
}

xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> count_actor = system.create_actor(*count_machine);
if (!count_actor.has_value()) {
    return 1;
}
const xstate::result<void> subscribed =
    system.subscribe(*count_actor, [](const xstate::snapshot& state) {
        std::cout << boost::json::serialize(state.context) << '\n';
    });
if (!subscribed.has_value()) {
    return 1;
}
if (!system.start(*count_actor).has_value()) {
    return 1;
}
if (!system.send(*count_actor, {.type = "increment", .payload = {{"value", 3}}}).has_value()) {
    return 1;
}
if (!system.send(*count_actor, {.type = "increment", .payload = {{"value", 2}}}).has_value()) {
    return 1;
}

It prints:

{"count":0,"unit":"clicks"}
{"count":3,"unit":"clicks"}
{"count":5,"unit":"clicks"}
  • The library merges the object an assigner returns into the context as Object.assign does: the members it returns are replaced or added, and the others kept, as unit is. XState’s two forms, an object of property assigners and a function returning part of the context, are both this one.

  • The assigner receives the context as a const reference and cannot mutate it, which XState’s documentation can only ask of you.

  • An assign is never returned to the caller: its effect is the new context. The actions after it in the same microstep see the context it left (Log shows it).

  • An assign needs a context that is an object. A config with no context, or a null one, starts with {}; any other value that is not an object (false, true, a number, a string, a list) is kept as written, and an assign on it fails the macrostep with implementation_failed. XState starts false, 0 and "" as {} too; an assign drops true or a nonzero number and spreads a string or a list into numbered members (A context that is not an object). An assigner that returns an error fails the macrostep with that error’s code; the assigner of addValue returns implementation_failed when the event has no integer value.

  • This machine has no states: its root takes the event increment with a transition that has actions and no target, and the action addValue handles it.

Context covers the context itself: its initial value, input and how a snapshot holds it.

Raise

A raise sends an event to the machine itself. The event goes to the macrostep’s internal queue, first in, first out. The machine takes it once no eventless transition is enabled, before the macrostep settles, so before any event from outside, which waits for the next macrostep (Eventless transitions). In the loader below, entering loading raises started, which carries the attempt the context holds:

const xstate::event_maker started =
    [](const xstate::action_args& args) -> xstate::result<xstate::event> {
    const boost::json::object* context = args.context.if_object();
    const boost::json::value* attempt =
        context == nullptr ? nullptr : context->if_contains("attempt");
    if (attempt == nullptr) {
        return xstate::failure<xstate::event>(xstate::errc::implementation_failed);
    }
    return xstate::event{.type = "started", .payload = {{"attempt", *attempt}}};
};
xstate::implementations implementations;
implementations.actions.emplace(
    "announce",
    xstate::raise_action{.event = started, .id = std::nullopt, .delay = std::nullopt});

const boost::json::value config = boost::json::parse(R"({
    "id": "loader",
    "initial": "idle",
    "context": {"attempt": 1},
    "states": {
        "idle": {"on": {"load": "loading"}},
        "loading": {"entry": "announce", "on": {"started": "running"}},
        "running": {}
    }
})");
const xstate::result<xstate::machine> loader = xstate::create_machine(config, implementations);
if (!loader.has_value()) {
    return 1;
}

const xstate::snapshot idle = xstate::get_initial_snapshot(*loader);
for (const xstate::microstep& step :
     xstate::get_microsteps(*loader, idle, {.type = "load", .payload = {}})) {
    std::cout << boost::json::serialize(step.snapshot.value);
    for (const xstate::action& one : step.actions) {
        std::cout << ' ' << one.type << ' ' << boost::json::serialize(one.params);
    }
    std::cout << '\n';
}

It prints, one line per microstep:

"loading" xstate.raise {"event":{"type":"started","attempt":1}}
"running"

load took two microsteps in one macrostep. The first entered loading, whose entry raised started; the second took started to running. The event a raise makes may depend on the context and the event: its event_maker receives action_args, as XState’s event function does.

A raise without a delay is returned too, as xstate.raise with its event, as XState returns it. It asks nothing of the caller: the macrostep has already queued the event.

Delayed raise, ids and cancel

With a delay, a raise is not queued: the event waits until the delay has passed, then starts a macrostep of its own. The delay is a number of milliseconds, or the name of a delay of implementations.delays, a function of action_args computed when the action resolves; a name the implementations do not hold is no delay, as in XState, and the event is raised at once. An id names the waiting event, and a cancel_action of the same id cancels it. Here scheduleNudge raises nudge after 1000 milliseconds, under the id nudge, and cancelNudge cancels it:

const xstate::event_maker nudge =
    [](const xstate::action_args&) -> xstate::result<xstate::event> {
    return xstate::event{.type = "nudge", .payload = {}};
};
const xstate::raise_action schedule_nudge{
    .event = nudge,
    .id = "nudge",
    .delay = xstate::delay_ref{std::uint64_t{1000}},
};
xstate::implementations implementations;
implementations.actions.emplace("scheduleNudge", schedule_nudge);
implementations.actions.emplace("cancelNudge", xstate::cancel_action{.id = "nudge"});

const boost::json::value config = boost::json::parse(R"({
    "id": "reminder",
    "initial": "waiting",
    "states": {
        "waiting": {
            "on": {
                "remind": {"actions": "scheduleNudge"},
                "dismiss": {"actions": "cancelNudge"},
                "nudge": "nudged"
            }
        },
        "nudged": {}
    }
})");
const xstate::result<xstate::machine> reminder =
    xstate::create_machine(config, implementations);
if (!reminder.has_value()) {
    return 1;
}

The machine core keeps no time. It returns the delayed raise and the cancel, with their params:

const xstate::snapshot waiting = xstate::get_initial_snapshot(*reminder);
for (const std::string_view type : {"remind", "dismiss"}) {
    const auto [next, actions] =
        xstate::transition(*reminder, waiting, {.type = std::string(type), .payload = {}});
    for (const xstate::action& one : actions) {
        std::cout << type << ": " << one.type << ' ' << boost::json::serialize(one.params)
                  << '\n';
    }
}

An actor keeps the timers, and the host brings the time:

xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> actor = system.create_actor(*reminder);
if (!actor.has_value() || !system.start(*actor).has_value()) {
    return 1;
}
const auto print_value = [&system, &actor](std::string_view when) {
    const xstate::snapshot* now = system.snapshot_of(*actor);
    if (now == nullptr) {
        return false;
    }
    std::cout << when << ": " << boost::json::serialize(now->value) << '\n';
    return true;
};
const xstate::event remind{.type = "remind", .payload = {}};
const xstate::event dismiss{.type = "dismiss", .payload = {}};

if (!system.send(*actor, remind).has_value() || !system.send(*actor, dismiss).has_value() ||
    !system.clock_tick(1000).has_value() || !print_value("at 1000")) {
    return 1;
}
if (!system.send(*actor, remind).has_value() || !system.clock_tick(2000).has_value() ||
    !print_value("at 2000")) {
    return 1;
}

It prints:

remind: xstate.raise {"event":{"type":"nudge"},"id":"nudge","delay":1000}
dismiss: xstate.cancel {"sendId":"nudge"}
at 1000: "waiting"
at 2000: "nudged"

The first nudge was cancelled before its time came; the second fired at 2000 and took the machine to nudged.

  • clock_tick(now) takes the time to move to, in milliseconds, here 1000 and then 2000, where XState’s SimulatedClock.increment takes the step to add. It fires the timers due by then one at a time, in the order of their deadlines, and runs the system after each.

  • With the pure functions, a simulated_clock is the caller’s clock: its apply schedules each built-in delayed xstate.raise a step returns and applies each built-in xstate.cancel, and ignores a custom action whatever its type, action::builtin telling them apart (Built-in and custom actions). Its pop_due gives back the due events one at a time, each to be sent as a macrostep of its own (Delayed events and the simulated clock).

  • A second timer armed under an id takes the id over: a cancel then finds the second, and the first still fires, as in XState.

  • A cancel finds a delayed send_to_action of the same actor by its id too (Sending to other actors).

  • A state’s after is built of these two actions: entering the state raises xstate.after.<delay>.<state id> with that delay, and leaving it cancels it (Delayed transitions). The raises follow the order in which XState reads the keys of after, the same as for states (Nested states and the root): an after that writes "1000" before "500" raises the 500 one first.

  • Time is whole milliseconds; XState’s SimulatedClock also takes fractions (Time in whole milliseconds).

Sending to other actors

send_to_action sends an event to another actor, as XState’s sendTo. send_parent_action sends one to the actor’s parent, as sendParent, and forward_to_action sends on the event being handled, as forwardTo. The child below counts the inc it receives and tells its parent each count:

const xstate::assigner add =
    [](const xstate::action_args& args) -> xstate::result<boost::json::object> {
    const std::optional<std::int64_t> count = integer_at(args.context.if_object(), "count");
    if (!count.has_value()) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"count", *count + 1}};
};
const xstate::assigner set_count =
    [](const xstate::action_args& args) -> xstate::result<boost::json::object> {
    const std::optional<std::int64_t> count = integer_at(&args.event.payload, "count");
    if (!count.has_value()) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"count", *count}};
};
const xstate::event_maker counted =
    [](const xstate::action_args& args) -> xstate::result<xstate::event> {
    const std::optional<std::int64_t> count = integer_at(args.context.if_object(), "count");
    if (!count.has_value()) {
        return xstate::failure<xstate::event>(xstate::errc::implementation_failed);
    }
    return xstate::event{.type = "counted", .payload = {{"count", *count}}};
};
xstate::implementations counter_implementations;
counter_implementations.actions.emplace("add", xstate::assign_action{.assignment = add});
counter_implementations.actions.emplace("setCount",
                                        xstate::assign_action{.assignment = set_count});
counter_implementations.actions.emplace(
    "report",
    xstate::send_parent_action{.event = counted, .id = std::nullopt, .delay = std::nullopt});

const boost::json::value counter_config = boost::json::parse(R"({
    "id": "counter",
    "context": {"count": 0},
    "on": {
        "inc": {"actions": ["add", "report"]},
        "set": {"actions": ["setCount", "report"]}
    }
})");
const xstate::result<xstate::machine> counter =
    xstate::create_machine(counter_config, counter_implementations);
if (!counter.has_value()) {
    return 1;
}

integer_at reads an integer member of an object, or nothing. The parent invokes the child, sends it inc on each click and forwards it set:

const xstate::event_maker inc =
    [](const xstate::action_args&) -> xstate::result<xstate::event> {
    return xstate::event{.type = "inc", .payload = {}};
};
const xstate::assigner keep_last =
    [](const xstate::action_args& args) -> xstate::result<boost::json::object> {
    const std::optional<std::int64_t> count = integer_at(&args.event.payload, "count");
    if (!count.has_value()) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"last", *count}};
};
const xstate::send_to_action send_inc{
    .target = "counter",
    .event = inc,
    .id = std::nullopt,
    .delay = std::nullopt,
};
const xstate::forward_to_action forward_set{
    .target = "counter",
    .id = std::nullopt,
    .delay = std::nullopt,
};
xstate::implementations app_implementations;
app_implementations.actors.emplace("counter", xstate::machine_actor{*counter});
app_implementations.actions.emplace("inc", send_inc);
app_implementations.actions.emplace("forwardSet", forward_set);
app_implementations.actions.emplace("keepLast", xstate::assign_action{.assignment = keep_last});

const boost::json::value app_config = boost::json::parse(R"({
    "id": "app",
    "context": {"last": null},
    "invoke": {"id": "counter", "src": "counter"},
    "on": {
        "click": {"actions": "inc"},
        "set": {"actions": "forwardSet"},
        "counted": {"actions": "keepLast"}
    }
})");
const xstate::result<xstate::machine> app =
    xstate::create_machine(app_config, app_implementations);
if (!app.has_value()) {
    return 1;
}

An actor of the parent takes three events, and the program prints both contexts after each:

xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> app_actor = system.create_actor(*app);
if (!app_actor.has_value() || !system.start(*app_actor).has_value()) {
    return 1;
}
const xstate::event click{.type = "click", .payload = {}};
const xstate::event set{.type = "set", .payload = {{"count", 10}}};
for (const xstate::event& sent : {click, click, set}) {
    if (!system.send(*app_actor, sent).has_value()) {
        return 1;
    }
    const std::optional<xstate::actor_ref> child = system.child_of(*app_actor, "counter");
    const xstate::snapshot* parent_now = system.snapshot_of(*app_actor);
    const xstate::snapshot* child_now =
        child.has_value() ? system.snapshot_of(*child) : nullptr;
    if (parent_now == nullptr || child_now == nullptr) {
        return 1;
    }
    std::cout << sent.type << ": app " << boost::json::serialize(parent_now->context)
              << ", counter " << boost::json::serialize(child_now->context) << '\n';
}

It prints:

click: app {"last":1}, counter {"count":1}
click: app {"last":2}, counter {"count":2}
set: app {"last":10}, counter {"count":10}

Each click made the parent send inc; the child counted and sent counted back, and the parent kept the count. set reached the child whole, its count with it.

A target is a string, read as XState reads one:

counter

The child of that id, an invoke’s or a spawned child’s.

#_counter

The same child, once it is in the snapshot’s children.

#_parent

The actor’s parent: send_parent_action sends there.

#_internal

The actor itself, which receives the event as one from outside, unlike a raised event.

#system:notifier

The actor registered under the systemId notifier, or the actor itself when none is. XState names it with a function, ({ system }) => system.get('notifier'), and sends to the actor itself when that returns nothing. A forwardTo fails the actor with unknown_target there instead, as XState’s development build throws.

  • The machine core checks a target when the action resolves. A child must be in the snapshot’s children, or, named by its bare id from an entry or initial action, be an invoke of the state being entered, whose child is spawned after the entry actions; otherwise the microstep fails with unknown_target. The parent, the actor itself and a systemId are its caller’s to find; XState’s pure functions find no parent there, and no child of the snapshot under its systemId (Built-in and custom actions). A forwardTo with an empty target fails with unknown_target.

  • The core returns xstate.sendTo with {targetId, event, id, delay}. XState’s params also hold the receiving actor, which is not JSON; here the caller finds it from targetId. The actor layer delivers the event once the sender’s macrostep has settled, so the receiver sees the sender’s new snapshot, as XState defers it for that reason.

  • With a delay, the sender arms a timer, as for a delayed raise, and a cancel_action of the same id cancels it.

  • A root actor has no parent: a send to #_parent fails it with unknown_target, as XState’s root actor fails with the error its resolution throws. The pure functions return the send all the same, as xstate.sendTo to #_parent, for their caller to deliver; XState’s pure functions throw on it, because they run the machine as an inert actor that has no parent (sendParent without a parent).

  • An actor reference is not data. Where XState puts self in an event or an input for the receiver to answer, xstate names the actor: by #_parent, by its id, or by a systemId (Sending events between actors).

  • Between actors, XState delivers an event to an idle actor at once, nested in the sender’s call stack, and queues it for a busy one. xactor queues every delivery: each actor has a mailbox, first in, first out, and the actors that have work wait in one queue, first in, first out, each turn handing one message to its actor. An actor receives what one sender sends it in the order sent, but the order between senders may differ from XState’s (Interleaving across actors).

Log

log_action writes a log line: XState’s log(value, label). Its value is a value_maker, a function of action_args that returns the value, or none for JavaScript’s undefined; without one, the value is {context, event}, as in XState. Its label is an optional string. The counter below logs a fixed message, its count under a label, and the default value:

const xstate::assigner add =
    [](const xstate::action_args& args) -> xstate::result<boost::json::object> {
    const boost::json::object* context = args.context.if_object();
    const boost::json::value* count =
        context == nullptr ? nullptr : context->if_contains("count");
    if (count == nullptr || !count->is_int64()) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"count", count->get_int64() + 1}};
};
const xstate::value_maker message =
    [](const xstate::action_args&) -> xstate::result<std::optional<boost::json::value>> {
    return std::optional<boost::json::value>("incremented");
};
const xstate::value_maker count =
    [](const xstate::action_args& args) -> xstate::result<std::optional<boost::json::value>> {
    const boost::json::object* context = args.context.if_object();
    const boost::json::value* found =
        context == nullptr ? nullptr : context->if_contains("count");
    if (found == nullptr) {
        return xstate::failure<std::optional<boost::json::value>>(
            xstate::errc::implementation_failed);
    }
    return std::optional<boost::json::value>(*found);
};
xstate::implementations implementations;
implementations.actions.emplace("add", xstate::assign_action{.assignment = add});
implementations.actions.emplace("logMessage",
                                xstate::log_action{.value = message, .label = std::nullopt});
implementations.actions.emplace("logCount",
                                xstate::log_action{.value = count, .label = "count"});
implementations.actions.emplace(
    "logAll", xstate::log_action{.value = std::nullopt, .label = std::nullopt});

const boost::json::value config = boost::json::parse(R"({
    "context": {"count": 0},
    "on": {
        "increment": {"actions": ["add", "logMessage", "logCount", "logAll"]}
    }
})");
const xstate::result<xstate::machine> counter = xstate::create_machine(config, implementations);
if (!counter.has_value()) {
    return 1;
}

The core returns each log as an action; an actor hands it to the callback given to on_log:

const xstate::event increment{.type = "increment", .payload = {}};

const xstate::snapshot initial = xstate::get_initial_snapshot(*counter);
const auto [next, actions] = xstate::transition(*counter, initial, increment);
for (const xstate::action& one : actions) {
    std::cout << one.type << ' ' << boost::json::serialize(one.params) << '\n';
}

xstate::actor_system system({.fuel = 10'000});
const xstate::result<void> logging = system.on_log(
    [](xstate::actor_ref, const boost::json::value* value, std::string_view label) {
        if (!label.empty()) {
            std::cout << label << ": ";
        }
        std::cout << (value == nullptr ? "undefined" : boost::json::serialize(*value)) << '\n';
    });
if (!logging.has_value()) {
    return 1;
}
const xstate::result<xstate::actor_ref> actor = system.create_actor(*counter);
if (!actor.has_value() || !system.start(*actor).has_value() ||
    !system.send(*actor, increment).has_value()) {
    return 1;
}

It prints the three actions transition returns, then the three lines the actor logs:

xstate.log {"value":"incremented"}
xstate.log {"value":1,"label":"count"}
xstate.log {"value":{"context":{"count":1},"event":{"type":"increment"}}}
"incremented"
count: 1
{"context":{"count":1},"event":{"type":"increment"}}
  • The core returns xstate.log with {value, label}, each left out when it has none. The actor layer hands each log to the callback given to on_log, with the actor, the value, a null pointer for undefined, and the label, empty when there is none: XState’s logger option, which receives (label, value) or (value).

  • A fixed message is a value_maker that returns it, as logMessage does for XState’s log('incremented').

  • logCount read the context after add: an action sees the context the assigns before it left.

  • The actor layer hands logs over as it hands custom actions, while the macrostep is resolved and under the same rules (Custom actions). on_log holds one callback for the whole system, which runs inside the actor’s turn as the callback of on_action does.

Emitting events

An actor emits an event for the code outside that listens to it, as an event emitter does: a machine reports what happened without knowing who listens. emit_action makes the event with an event_maker, so it may depend on the context:

const xstate::event_maker static_event =
    [](const xstate::action_args&) -> xstate::result<xstate::event> {
    return xstate::event{.type = "someStaticEvent", .payload = {{"data", 42}}};
};
const xstate::event_maker dynamic_event =
    [](const xstate::action_args& args) -> xstate::result<xstate::event> {
    const boost::json::object* context = args.context.if_object();
    const boost::json::value* data =
        context == nullptr ? nullptr : context->if_contains("someData");
    if (data == nullptr) {
        return xstate::failure<xstate::event>(xstate::errc::implementation_failed);
    }
    return xstate::event{.type = "someDynamicEvent", .payload = {{"data", *data}}};
};
xstate::implementations implementations;
implementations.actions.emplace("emitStaticEvent", xstate::emit_action{.event = static_event});
implementations.actions.emplace("emitDynamicEvent",
                                xstate::emit_action{.event = dynamic_event});

const boost::json::value config = boost::json::parse(R"({
    "context": {"someData": "hello"},
    "on": {
        "someEvent": {"actions": ["emitStaticEvent", "emitDynamicEvent", "track"]}
    }
})");
const xstate::result<xstate::machine> machine = xstate::create_machine(config, implementations);
if (!machine.has_value()) {
    return 1;
}

The listeners are the host’s: on_emitted(actor, listener) is XState’s actor.on('*', listener):

xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> actor = system.create_actor(*machine);
if (!actor.has_value()) {
    return 1;
}
const xstate::result<void> hearing_static =
    system.on_emitted(*actor, [](const xstate::event& emitted) {
        if (emitted.type == "someStaticEvent") {
            std::cout << "static: " << boost::json::serialize(xstate::to_json(emitted)) << '\n';
        }
    });
if (!hearing_static.has_value()) {
    return 1;
}
const xstate::result<void> hearing_any = system.on_emitted(
    *actor, [](const xstate::event& emitted) { std::cout << "any: " << emitted.type << '\n'; });
if (!hearing_any.has_value()) {
    return 1;
}
const xstate::result<void> acting =
    system.on_action([](xstate::actor_ref, const xstate::action& custom) {
        std::cout << "action: " << custom.type << '\n';
    });
if (!acting.has_value()) {
    return 1;
}

if (!system.start(*actor).has_value() ||
    !system.send(*actor, {.type = "someEvent", .payload = {}}).has_value()) {
    return 1;
}

It prints:

action: track
static: {"type":"someStaticEvent","data":42}
any: someStaticEvent
any: someDynamicEvent
  • on_emitted hears every event the actor emits. XState also takes the type to listen to, actor.on('someStaticEvent', ...); in xstate a listener compares type, as the first one does. XState calls the listeners of the event’s type before those of *, whatever the order they were added in, and xstate calls all of them in the order added: add the listeners that stand for typed ones first, as here, to keep XState’s order (Listeners of emitted events).

  • An actor may have several listeners, and an inspector hears each emitted event too (Inspection). xstate has no unsubscribe. on_emitted returns xactor’s invalid_argument for an address that names no actor. A listener runs inside the actor’s turn, as the callback of on_action does: a call from it that changes the system, on_emitted among them, returns invalid_argument too and changes nothing (A callback that calls the system back).

  • The core returns xstate.emit with {event}. The actor layer hands the event to the listeners once the macrostep has settled, as XState defers it: after track, a custom action listed after both emits.

  • An emitted event of the type * reaches a listener once; XState hands it twice to a listener on * (An emitted event of the type *).

  • XState’s promise, callback, observable and transition actors emit through an emit function they receive. In xstate only a machine emits, with emit_action; a host actor has no way to.

Input and output

Input is the data an actor is given when it is created. Output is the data an actor produces when it is done: the result of its work. Between the two sit the done events, which tell a state or a parent actor that what it waited on has finished, and carry the output to it.

Input exists so that one machine can start from different data: a feedback form for one user or another, a conversion of one amount or another. Without it, each starting value needs a machine of its own, built by a factory function. Output exists so that whoever waits on an actor receives its result without reading the actor’s context.

In xstate, as in XState, input and output are values, here JSON values. A value that depends on nothing stays in the config, where XState’s JSON puts it. A value XState computes with a function (a context from the input, an invoke’s input from the context, an output from the context) needs a function JSON cannot hold, so the caller registers it in implementations, as it registers actions and guards (Implementations).

An invoke’s computed input and a state’s computed output are registered under the id of the invoke or the state, and not under a name the config refers to, as an action is. In XState’s JSON an invoke’s input and a state’s output are the values themselves: a string there is the input, not the name of a function. Keyed by id, the function leaves the config as XState reads it.

A machine’s input

An actor is created with its input. XState’s createActor(machine, { input }) is actor_system::create_actor(machine, options), whose actor_options::input is a boost::json::value, null when none is given. A machine usually reads its input once, to compute its initial context: XState’s context: ({ input }) => ... is implementations.context, a context_maker that receives the input and returns the initial context, or an error.

/** The context a feedback machine starts with, read from its input. */
xstate::result<boost::json::value> feedback_context(const boost::json::value& input) {
    const boost::json::object* given = input.if_object();
    const boost::json::value* user_id = given == nullptr ? nullptr : given->if_contains("userId");
    const boost::json::value* rating =
        given == nullptr ? nullptr : given->if_contains("defaultRating");
    if (user_id == nullptr || rating == nullptr) {
        return xstate::failure<boost::json::value>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"userId", *user_id}, {"feedback", ""}, {"rating", *rating}};
}
xstate::implementations implementations;
implementations.context = feedback_context;
const boost::json::value config = boost::json::parse(R"({
    "id": "feedback",
    "initial": "prompt",
    "states": {
        "prompt": {}
    }
})");
const xstate::result<xstate::machine> feedback_machine =
    xstate::create_machine(config, implementations);
if (!feedback_machine.has_value()) {
    return 1;
}

xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> feedback_actor = system.create_actor(
    *feedback_machine, {.input = boost::json::object{{"userId", "123"}, {"defaultRating", 5}}});
if (!feedback_actor.has_value() || !system.start(*feedback_actor).has_value()) {
    return 1;
}
const xstate::snapshot* started = system.snapshot_of(*feedback_actor);
if (started == nullptr) {
    return 1;
}
std::cout << boost::json::serialize(started->context) << '\n';

It prints:

{"userId":"123","feedback":"","rating":5}

feedback_context checks the input before it reads it, and returns an error when the input lacks a member it needs, where XState’s function would read undefined. When implementations.context is set, the context it returns replaces the config’s context; it must be an object. Its other rules, and a machine started without the input it needs, are in Initial context from the input. XState also hands the context function spawn and self; xstate hands it the input only, so a context cannot spawn a child (The context function).

The actor copies the input when it is created and reads it once, to begin its initial macrostep: it keeps no link to the value the caller passed. To hand a running actor new data, send it an event (Events and transitions).

Input without an actor

The machine core takes an input too. Every function that starts a machine takes it as its last argument, null when it is left out: initial_transition, get_initial_snapshot and get_initial_microsteps, which are XState’s initialTransition, getInitialSnapshot and getInitialMicrosteps, and begin_initial, the cursor of the initial macrostep, which the other three run to its end (The initial macrostep). Input runs them on a machine whose context needs the input.

const boost::json::object input{{"userId", "123"}, {"defaultRating", 5}};

const xstate::snapshot started = xstate::initial_transition(*feedback_machine, input).first;
std::cout << boost::json::serialize(started.context) << '\n';

const std::vector<xstate::microstep> microsteps =
    xstate::get_initial_microsteps(*feedback_machine, input);
if (microsteps.empty()) {
    return 1;
}
std::cout << boost::json::serialize(microsteps.back().snapshot.context) << " after "
          << microsteps.size() << " microstep\n";

It prints:

{"userId":"123","feedback":"","rating":5}
{"userId":"123","feedback":"","rating":5} after 1 microstep

The machine is the feedback machine above, run here without an actor. Its initial macrostep takes one microstep, the one that enters prompt, and get_initial_microsteps lists it with the context computed from the input, as XState’s getInitialMicrosteps does.

The xstate.init event

A machine’s initial macrostep runs for the event xstate.init. When the machine has an input, the event carries it as its member input, so every action that runs for xstate.init can read it from the event: the entry actions and initial actions of the states the machine starts in, as this entry action does, and the eventless transitions taken before any raised event, with the entry actions of the states they enter. An action that runs for an event raised during the initial macrostep, a done event among them, receives that event, which has no input.

Where XState writes the entry action as a function of the event, ({ event }) => ..., here it is a named log_action whose value is the event, read from its action_args; the actor system hands each value logged to its on_log listener (Log).

xstate::implementations implementations;
// XState's ({ event }) => console.log(event), as a log of the event.
const xstate::value_maker the_event =
    [](const xstate::action_args& args) -> xstate::result<std::optional<boost::json::value>> {
    return boost::json::value(xstate::to_json(args.event));
};
implementations.actions.emplace("logEvent",
                                xstate::log_action{.value = the_event, .label = std::nullopt});
const boost::json::value config = boost::json::parse(R"({"entry": "logEvent"})");
const xstate::result<xstate::machine> feedback_machine =
    xstate::create_machine(config, implementations);
if (!feedback_machine.has_value()) {
    return 1;
}

xstate::actor_system system({.fuel = 10'000});
const xstate::result<void> logging =
    system.on_log([](xstate::actor_ref, const boost::json::value* value, std::string_view) {
        if (value != nullptr) {
            std::cout << boost::json::serialize(*value) << '\n';
        }
    });
if (!logging.has_value()) {
    return 1;
}
const xstate::result<xstate::actor_ref> feedback_actor = system.create_actor(
    *feedback_machine, {.input = boost::json::object{{"userId", "123"}, {"defaultRating", 5}}});
if (!feedback_actor.has_value() || !system.start(*feedback_actor).has_value()) {
    return 1;
}

It prints:

{"type":"xstate.init","input":{"userId":"123","defaultRating":5}}

XState’s xstate.init always has the member input, undefined when there is no input, which JSON.stringify leaves out. xstate’s has it only for an input that is not null, since JSON has no undefined: given the input null, XState writes "input": null and xstate writes no input. For the same reason a machine started without an input hands its context function null, where XState hands it undefined.

Input to an invoked actor

A state that invokes an actor can give it an input. A fixed input is a value in the invoke’s config, "input", which the child receives as it is when the state is entered. A host actor, xstate’s stand-in for XState’s fromPromise, runs no code: the host reads its input from its pending request, host_request::input (Host actors). A machine child receives it as a machine created with an input does, in its implementations.context and its xstate.init event (Invoking actors).

xstate::implementations implementations;
// The fetch is the host's: a host actor, XState's fromPromise.
implementations.actors.emplace("liveFeedback", xstate::host_actor{});
const boost::json::value config = boost::json::parse(R"({
    "id": "feedback",
    "invoke": {
        "src": "liveFeedback",
        "input": {"domain": "stately.ai"}
    }
})");
const xstate::result<xstate::machine> feedback_machine =
    xstate::create_machine(config, implementations);
if (!feedback_machine.has_value()) {
    return 1;
}

xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> feedback_actor = system.create_actor(*feedback_machine);
if (!feedback_actor.has_value() || !system.start(*feedback_actor).has_value()) {
    return 1;
}
for (const xstate::host_request& request : system.host_requests()) {
    if (request.input.has_value()) {
        std::cout << boost::json::serialize(*request.input) << '\n';
    }
}

It prints:

{"domain":"stately.ai"}

The host reads the input from the pending request and does the work itself, where XState’s fromPromise receives the input as an argument of its function; the host then answers the request with actor_system::resolve (Actor done events).

An input computed from the context and the event

XState also computes an invoke’s input with a function, input: ({ context, event }) => ...; in xstate it is a value_maker registered in implementations.inputs under the invoke’s id. It receives the action_args every implementation receives, the context, the event that entered the state and null params, and returns the input, none for undefined (When a value is undefined), or an error.

// assign({ userId: ({ event }) => event.userId })
const xstate::assigner select_user =
    [](const xstate::action_args& args) -> xstate::result<boost::json::object> {
    const boost::json::value* user_id = args.event.payload.if_contains("userId");
    if (user_id == nullptr) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"userId", *user_id}};
};
// ({ context }) => ({ userId: context.userId })
const xstate::value_maker user_of_context =
    [](const xstate::action_args& args) -> xstate::result<std::optional<boost::json::value>> {
    const boost::json::object* context = args.context.if_object();
    const boost::json::value* user_id =
        context == nullptr ? nullptr : context->if_contains("userId");
    if (user_id == nullptr) {
        return xstate::failure<std::optional<boost::json::value>>(
            xstate::errc::implementation_failed);
    }
    return boost::json::value(boost::json::object{{"userId", *user_id}});
};

xstate::implementations implementations;
implementations.actors.emplace("fetchUser", xstate::host_actor{});
implementations.actions.emplace("selectUser", xstate::assign_action{.assignment = select_user});
// The input of the invoke whose id is fetchUser.
implementations.inputs.emplace("fetchUser", user_of_context);
const boost::json::value config = boost::json::parse(R"({
    "id": "feedback",
    "context": {"userId": ""},
    "initial": "idle",
    "states": {
        "idle": {"on": {"user.selected": "loading"}},
        "loading": {
            "entry": "selectUser",
            "invoke": {"id": "fetchUser", "src": "fetchUser"}
        }
    }
})");
const xstate::result<xstate::machine> feedback_machine =
    xstate::create_machine(config, implementations);
if (!feedback_machine.has_value()) {
    return 1;
}

xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> feedback_actor = system.create_actor(*feedback_machine);
if (!feedback_actor.has_value() || !system.start(*feedback_actor).has_value()) {
    return 1;
}
const xstate::event selected{.type = "user.selected", .payload = {{"userId", "42"}}};
if (!system.send(*feedback_actor, selected).has_value()) {
    return 1;
}
for (const xstate::host_request& request : system.host_requests()) {
    if (request.input.has_value()) {
        std::cout << boost::json::serialize(*request.input) << '\n';
    }
}

It prints:

{"userId":"42"}

The input is computed when its state is entered, after the state’s entry actions and before its initial actions, as XState’s enterStates resolves them. So the input reads the user id the entry assign has just stored. The event is the one that entered the state, user.selected, which an input function could read directly as well.

The function is found by the invoke’s id, so give the invoke an id. Without one, its id is <index>.<state id>, the invoke’s position among the state’s invokes and the state’s id, as XState’s createInvokeId makes it, and the function must be registered under that: here 0.feedback.loading. Creating the machine refuses a computed input that names no invoke, and one for an invoke whose config fixes an input too (Rules).

Input to a spawned actor

A child spawned by an action receives its input from the action. XState’s spawnChild(src, { id, input }) is a spawn_child_action registered under a name, whose input is a value_maker computed when the action resolves, from the context and the event of its microstep. Without one, the child has no input; a fixed input is a function that returns it.

Stately’s page spawns inside assign, to keep the child’s reference in the context. xstate has no spawn inside assign, and an actor reference is not data: the parent names its child by its id (Spawning actors).

// ({ context }) => ({ userId: context.userId })
const xstate::value_maker user_of_context =
    [](const xstate::action_args& args) -> xstate::result<std::optional<boost::json::value>> {
    const boost::json::object* context = args.context.if_object();
    const boost::json::value* user_id =
        context == nullptr ? nullptr : context->if_contains("userId");
    if (user_id == nullptr) {
        return xstate::failure<std::optional<boost::json::value>>(
            xstate::errc::implementation_failed);
    }
    return boost::json::value(boost::json::object{{"userId", *user_id}});
};

// spawnChild('emailUser', { id: 'emailUser', input: ... })
const xstate::spawn_child_action spawn_email_user{
    .src = "emailUser",
    .id = "emailUser",
    .system_id = std::nullopt,
    .input = user_of_context,
};

xstate::implementations implementations;
implementations.actors.emplace("emailUser", xstate::host_actor{});
implementations.actions.emplace("spawnEmailUser", spawn_email_user);
const boost::json::value config = boost::json::parse(R"({
    "id": "feedback",
    "context": {"userId": "123"},
    "on": {
        "feedback.submit": {"actions": "spawnEmailUser"}
    }
})");
const xstate::result<xstate::machine> feedback_machine =
    xstate::create_machine(config, implementations);
if (!feedback_machine.has_value()) {
    return 1;
}

xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> feedback_actor = system.create_actor(*feedback_machine);
if (!feedback_actor.has_value() || !system.start(*feedback_actor).has_value()) {
    return 1;
}
if (!system.send(*feedback_actor, {.type = "feedback.submit", .payload = {}}).has_value()) {
    return 1;
}
for (const xstate::host_request& request : system.host_requests()) {
    if (request.input.has_value()) {
        std::cout << boost::json::serialize(*request.input) << '\n';
    }
}

It prints:

{"userId":"123"}

The machine core returns the spawn as an xstate.spawnChild action whose params hold the child’s id and src, and its input and systemId when it has them; the actor layer creates the child from them. An invoke returns the same action when its state is entered.

Output

A machine’s output is the value it is done with. It is the output of its root, computed when the machine is done, when a top-level final state is entered or each region of a parallel root is in a final state (When the machine is done), and from then on it is the snapshot’s output, a std::optional<boost::json::value> that holds nothing while the machine is active. The root’s output is fixed in the config, the root’s "output", or computed: XState’s output: ({ context }) => ... at the root of the config is a value_maker registered in implementations.outputs under the root’s id.

// ({ input }) => ({ amount: input.amount, currency: input.toCurrency })
const xstate::context_maker from_input =
    [](const boost::json::value& input) -> xstate::result<boost::json::value> {
    const boost::json::object* given = input.if_object();
    const boost::json::value* amount =
        given == nullptr ? nullptr : given->if_contains("amount");
    const boost::json::value* currency =
        given == nullptr ? nullptr : given->if_contains("toCurrency");
    if (amount == nullptr || currency == nullptr) {
        return xstate::failure<boost::json::value>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"amount", *amount}, {"currency", *currency}};
};
// assign({ amount: ({ event }) => event.amount })
const xstate::assigner keep_amount =
    [](const xstate::action_args& args) -> xstate::result<boost::json::object> {
    const boost::json::value* amount = args.event.payload.if_contains("amount");
    if (amount == nullptr) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"amount", *amount}};
};
// ({ context }) => ({ amount: context.amount, currency: context.currency })
const xstate::value_maker from_context =
    [](const xstate::action_args& args) -> xstate::result<std::optional<boost::json::value>> {
    const boost::json::object* context = args.context.if_object();
    const boost::json::value* amount =
        context == nullptr ? nullptr : context->if_contains("amount");
    const boost::json::value* currency =
        context == nullptr ? nullptr : context->if_contains("currency");
    if (amount == nullptr || currency == nullptr) {
        return xstate::failure<std::optional<boost::json::value>>(
            xstate::errc::implementation_failed);
    }
    return boost::json::value(
        boost::json::object{{"amount", *amount}, {"currency", *currency}});
};

xstate::implementations implementations;
implementations.context = from_input;
implementations.actions.emplace("keepAmount", xstate::assign_action{.assignment = keep_amount});
// The machine's output: the output of its root, whose id is currency.
implementations.outputs.emplace("currency", from_context);
const boost::json::value config = boost::json::parse(R"({
    "id": "currency",
    "initial": "converting",
    "states": {
        "converting": {
            "on": {
                "amount.converted": {"target": "converted", "actions": "keepAmount"}
            }
        },
        "converted": {"type": "final"}
    }
})");
const xstate::result<xstate::machine> currency_machine =
    xstate::create_machine(config, implementations);
if (!currency_machine.has_value()) {
    return 1;
}

xstate::actor_system system({.fuel = 10'000});
const boost::json::object input{{"amount", 10}, {"fromCurrency", "USD"}, {"toCurrency", "EUR"}};
const xstate::result<xstate::actor_ref> currency_actor =
    system.create_actor(*currency_machine, {.input = input});
if (!currency_actor.has_value()) {
    return 1;
}
const xstate::result<void> subscribed =
    system.subscribe(*currency_actor, [](const xstate::snapshot& snapshot) {
        if (snapshot.status == xstate::status::done && snapshot.output.has_value()) {
            std::cout << boost::json::serialize(*snapshot.output) << '\n';
        }
    });
if (!subscribed.has_value()) {
    return 1;
}
const xstate::event converted{.type = "amount.converted", .payload = {{"amount", 12}}};
if (!system.start(*currency_actor).has_value() ||
    !system.send(*currency_actor, converted).has_value()) {
    return 1;
}

It prints:

{"amount":12,"currency":"EUR"}

subscribe hands its listener each snapshot the actor settles in while it is active, and the one it is done in, as XState’s subscribe does; only the done one holds the output.

The root’s id is the config’s "id", (machine) when the config has none, as in XState; a machine whose output is computed is clearer with an id of its own.

A final state’s output

A final state can have an output too. When it is entered, its parent state is done (A parent state is done), and the parent’s done event, xstate.done.state.<parent id>, carries the final state’s output as its member output; the parent’s onDone reads it from the event (Done events). It is fixed in the final state’s config, or computed by a value_maker registered in implementations.outputs under the final state’s id, from the context and the event that entered the final state.

// ({ event }) => ({ grams: event.grams }), from the event that entered the final state
const xstate::value_maker dose_of_event =
    [](const xstate::action_args& args) -> xstate::result<std::optional<boost::json::value>> {
    const boost::json::value* grams = args.event.payload.if_contains("grams");
    if (grams == nullptr) {
        return xstate::failure<std::optional<boost::json::value>>(
            xstate::errc::implementation_failed);
    }
    return boost::json::value(boost::json::object{{"grams", *grams}});
};
// assign({ dose: ({ event }) => event.output })
const xstate::assigner keep_dose =
    [](const xstate::action_args& args) -> xstate::result<boost::json::object> {
    const boost::json::value* output = args.event.payload.if_contains("output");
    if (output == nullptr) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"dose", *output}};
};

xstate::implementations implementations;
implementations.actions.emplace("keepDose", xstate::assign_action{.assignment = keep_dose});
// The output of the final state whose id is coffee.preparation.ready.
implementations.outputs.emplace("coffee.preparation.ready", dose_of_event);
const boost::json::value config = boost::json::parse(R"({
    "id": "coffee",
    "context": {"dose": null},
    "initial": "preparation",
    "states": {
        "preparation": {
            "initial": "weighing",
            "states": {
                "weighing": {"on": {"weighed": "grinding"}},
                "grinding": {"on": {"ground": "ready"}},
                "ready": {"type": "final"}
            },
            "onDone": {"target": "brewing", "actions": "keepDose"}
        },
        "brewing": {}
    }
})");
const xstate::result<xstate::machine> coffee_machine =
    xstate::create_machine(config, implementations);
if (!coffee_machine.has_value()) {
    return 1;
}

const xstate::event weighed{.type = "weighed", .payload = {}};
const xstate::event ground{.type = "ground", .payload = {{"grams", 18}}};
xstate::snapshot snapshot = xstate::initial_transition(*coffee_machine).first;
snapshot = xstate::transition(*coffee_machine, snapshot, weighed).first;
snapshot = xstate::transition(*coffee_machine, snapshot, ground).first;
std::cout << boost::json::serialize(snapshot.value) << ' '
          << boost::json::serialize(snapshot.context) << '\n';

It prints:

"brewing" {"dose":{"grams":18}}

ready has no id of its own, so its id is the root’s id and its path, joined by dots, coffee.preparation.ready (State nodes and ids). Its output reads grams from ground, the event that entered it, and keepDose reads the output from the done event’s payload.

The machine’s output and its final state

When the root has an output, it is computed as the machine completes, from the context and the done event that completed it, as XState’s internal getMachineOutput (stateUtils.ts) computes it. For a compound root, that is the done event of the top-level final state that completed the machine, which carries that final state’s own output. So the root’s output can pass on the output of the final state that completed the machine, as ({ event }) => event.output; here it returns the whole event, to show it. When the root has no output, the machine has none, whatever its final state’s output.

/** Pays a checkout and returns its output: "error" when it is not done, "none" without one. */
std::string checkout_output(const xstate::machine& checkout) {
    const xstate::event paid{.type = "paid", .payload = {{"receiptId", "R-1"}}};
    const xstate::snapshot start = xstate::initial_transition(checkout).first;
    const xstate::snapshot done = xstate::transition(checkout, start, paid).first;
    if (done.status != xstate::status::done) {
        return "error";
    }
    if (!done.output.has_value()) {
        return "none";
    }
    return boost::json::serialize(*done.output);
}
// ({ event }) => ({ receiptId: event.receiptId })
const xstate::value_maker receipt_of_event =
    [](const xstate::action_args& args) -> xstate::result<std::optional<boost::json::value>> {
    const boost::json::value* receipt_id = args.event.payload.if_contains("receiptId");
    if (receipt_id == nullptr) {
        return xstate::failure<std::optional<boost::json::value>>(
            xstate::errc::implementation_failed);
    }
    return boost::json::value(boost::json::object{{"receiptId", *receipt_id}});
};
// ({ event }) => event
const xstate::value_maker the_event =
    [](const xstate::action_args& args) -> xstate::result<std::optional<boost::json::value>> {
    return boost::json::value(xstate::to_json(args.event));
};
const boost::json::value config = boost::json::parse(R"({
    "id": "checkout",
    "initial": "paying",
    "states": {
        "paying": {"on": {"paid": "receipt"}},
        "receipt": {"type": "final"}
    }
})");

xstate::implementations without_root_output;
without_root_output.outputs.emplace("checkout.receipt", receipt_of_event);
xstate::implementations with_root_output = without_root_output;
with_root_output.outputs.emplace("checkout", the_event);

const xstate::result<xstate::machine> with_output =
    xstate::create_machine(config, with_root_output);
const xstate::result<xstate::machine> without_output =
    xstate::create_machine(config, without_root_output);
if (!with_output.has_value() || !without_output.has_value()) {
    return 1;
}
std::cout << checkout_output(*with_output) << '\n' << checkout_output(*without_output) << '\n';

It prints:

{"type":"xstate.done.state.checkout.receipt","output":{"receiptId":"R-1"}}
none

The event names the final state, checkout.receipt, not its parent: it exists for the root’s output only, and no transition takes it. A machine whose root is a parallel state completes when each of its regions is done, and the event is then the root’s own, xstate.done.state.<root id>, without an output.

When a value is undefined

JavaScript has undefined, and JSON has not. A value_maker returns std::optional<boost::json::value>: none stands for undefined, and the member that would hold it is left out, as JavaScript’s JSON.stringify leaves it out. The snapshot then has no output, a done event no output, a child no input (host_request::input holds nothing, and the xstate.spawnChild params have no input), a log no value.

A JSON null is a value like any other, and is kept: a null output, a done event’s null output, a host actor’s null input. The exception is a machine’s input: for a machine, a null input and no input are the same, and its xstate.init has no input (The xstate.init event, undefined).

/** A snapshot's output: "error" when it is not done, "none" when it has none. */
std::string output_of(const xstate::snapshot& done) {
    if (done.status != xstate::status::done) {
        return "error";
    }
    if (!done.output.has_value()) {
        return "none";
    }
    return boost::json::serialize(*done.output);
}

// ({ input }) => input
const xstate::context_maker the_input =
    [](const boost::json::value& input) -> xstate::result<boost::json::value> { return input; };
// ({ context }) => context.total, undefined when the context has no total
const xstate::value_maker total_of_context =
    [](const xstate::action_args& args) -> xstate::result<std::optional<boost::json::value>> {
    const boost::json::object* context = args.context.if_object();
    const boost::json::value* total =
        context == nullptr ? nullptr : context->if_contains("total");
    if (total == nullptr) {
        return std::optional<boost::json::value>();
    }
    return std::optional<boost::json::value>(*total);
};

xstate::implementations implementations;
implementations.context = the_input;
implementations.outputs.emplace("order", total_of_context);
const boost::json::value config = boost::json::parse(R"({
    "id": "order",
    "initial": "placed",
    "states": {
        "placed": {"type": "final"}
    }
})");
const xstate::result<xstate::machine> order_machine =
    xstate::create_machine(config, implementations);
if (!order_machine.has_value()) {
    return 1;
}

const std::array<boost::json::object, 3> inputs{
    boost::json::object{{"total", 42}},
    boost::json::object{},
    boost::json::object{{"total", nullptr}},
};
for (const boost::json::object& input : inputs) {
    const xstate::snapshot done = xstate::initial_transition(*order_machine, input).first;
    std::cout << boost::json::serialize(input) << " -> " << output_of(done) << '\n';
}

It prints:

{"total":42} -> 42
{} -> none
{"total":null} -> null

The order machine’s initial state is final, so the machine is done as soon as it starts. A host actor resolved with std::nullopt is a promise resolved with undefined in the same way: its done event has no output.

Done events

When something a machine waits on finishes, the machine hears it as an event, and onDone is the transition that takes it. A done event lets a parent move on when what it started has finished, without knowing the steps that finished it: the coffee machine’s preparation moves to brewing once its steps reach ready, and the order below moves on once the child that prices it is done. There are two kinds:

Event Raised or sent when Members besides type

xstate.done.state.<id>

the state <id> is done: a final child of a compound state is entered, or each region of a parallel state is in a final state

output, the final child’s output, when it has one; none for a parallel state

xstate.done.actor.<id>

the child actor whose id is <id>, invoked or spawned, is done

output, the child’s output, when it has one; actorId, the child’s id

A state’s onDone is its transition on xstate.done.state.<state id>. An invoke’s onDone is a transition of the invoking state on xstate.done.actor.<invoke id>, and its onError one on xstate.error.actor.<invoke id>, the event a child that fails sends with its error and its actorId (Done and error events). One state can have both kinds of onDone: its own, for its final child, and its invoke’s, for its child actor. A spawned child has no invoke to hold an onDone; its parent takes the child’s done event with a transition like any other, "on": {"xstate.done.actor.<id>": ...}.

State done events

A state done event is raised by the machine itself, into the macrostep’s internal queue, and runs in the same macrostep as the microstep that entered the final state; nobody sends it. The macrostep cursor shows it, since each microstep reports the event it ran for. This runs ground on the coffee machine above, from grinding:

for (const xstate::microstep& step :
     xstate::get_microsteps(*coffee_machine, snapshot, ground)) {
    std::cout << boost::json::serialize(xstate::to_json(step.event)) << " -> "
              << boost::json::serialize(step.snapshot.value) << '\n';
}

It prints:

{"type":"ground","grams":18} -> {"preparation":"ready"}
{"type":"xstate.done.state.coffee.preparation","output":{"grams":18}} -> "brewing"

The first microstep enters ready, which raises xstate.done.state.coffee.preparation with its output; the second takes the onDone of preparation to brewing. XState’s getMicrosteps returns each microstep’s snapshot and actions, but not its event, which XState shows only to an inspector, in its @xstate.microstep events.

A parallel state is done when each of its regions is in a final state (A parallel state is done). A region that is a compound state raises its own done event when its final child is entered, with that child’s output, and the parallel state’s done event follows, without an output.

Actor done events

A child actor is done when its machine is done (When the machine is done), for a machine child, or when the host answers its request with actor_system::resolve(request, output), for a host actor: the output it passes is the actor’s output, none for XState’s undefined (Host actors). Its parent then receives xstate.done.actor.<id>, with the child’s output and its id. Here a child machine computes a quote from its input, and its parent takes the quote in the invoke’s onDone:

// ({ input }) => input
const xstate::context_maker the_input =
    [](const boost::json::value& input) -> xstate::result<boost::json::value> { return input; };
// ({ context }) => ({ total: context.items * context.unitPrice })
const xstate::value_maker total_of_context =
    [](const xstate::action_args& args) -> xstate::result<std::optional<boost::json::value>> {
    const boost::json::object* context = args.context.if_object();
    const boost::json::value* items =
        context == nullptr ? nullptr : context->if_contains("items");
    const boost::json::value* unit_price =
        context == nullptr ? nullptr : context->if_contains("unitPrice");
    if (items == nullptr || unit_price == nullptr || !items->is_int64() ||
        !unit_price->is_int64()) {
        return xstate::failure<std::optional<boost::json::value>>(
            xstate::errc::implementation_failed);
    }
    return boost::json::value(
        boost::json::object{{"total", items->get_int64() * unit_price->get_int64()}});
};

xstate::implementations quote_implementations;
quote_implementations.context = the_input;
quote_implementations.outputs.emplace("quote", total_of_context);
const boost::json::value quote_config = boost::json::parse(R"({
    "id": "quote",
    "initial": "priced",
    "states": {
        "priced": {"type": "final"}
    }
})");
const xstate::result<xstate::machine> quote_machine =
    xstate::create_machine(quote_config, quote_implementations);
if (!quote_machine.has_value()) {
    return 1;
}
// ({ event }) => console.log(event), as a log of the event
const xstate::value_maker the_event =
    [](const xstate::action_args& args) -> xstate::result<std::optional<boost::json::value>> {
    return boost::json::value(xstate::to_json(args.event));
};
// assign({ quote: ({ event }) => event.output })
const xstate::assigner keep_quote =
    [](const xstate::action_args& args) -> xstate::result<boost::json::object> {
    const boost::json::value* output = args.event.payload.if_contains("output");
    if (output == nullptr) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"quote", *output}};
};

xstate::implementations order_implementations;
order_implementations.actors.emplace("quote", xstate::machine_actor{*quote_machine});
order_implementations.actions.emplace(
    "logEvent", xstate::log_action{.value = the_event, .label = std::nullopt});
order_implementations.actions.emplace("keepQuote",
                                      xstate::assign_action{.assignment = keep_quote});
const boost::json::value order_config = boost::json::parse(R"({
    "id": "order",
    "context": {"quote": null},
    "initial": "quoting",
    "states": {
        "quoting": {
            "invoke": {
                "id": "quote",
                "src": "quote",
                "input": {"items": 3, "unitPrice": 4},
                "onDone": {"target": "quoted", "actions": ["logEvent", "keepQuote"]}
            }
        },
        "quoted": {}
    }
})");
const xstate::result<xstate::machine> order_machine =
    xstate::create_machine(order_config, order_implementations);
if (!order_machine.has_value()) {
    return 1;
}

xstate::actor_system system({.fuel = 10'000});
const xstate::result<void> logging =
    system.on_log([](xstate::actor_ref, const boost::json::value* value, std::string_view) {
        if (value != nullptr) {
            std::cout << boost::json::serialize(*value) << '\n';
        }
    });
if (!logging.has_value()) {
    return 1;
}
const xstate::result<xstate::actor_ref> order = system.create_actor(*order_machine);
if (!order.has_value() || !system.start(*order).has_value()) {
    return 1;
}
const xstate::snapshot* quoted = system.snapshot_of(*order);
if (quoted == nullptr) {
    return 1;
}
std::cout << boost::json::serialize(quoted->value) << ' '
          << boost::json::serialize(quoted->context) << '\n';

It prints:

{"type":"xstate.done.actor.quote","output":{"total":12},"actorId":"quote"}
"quoted" {"quote":{"total":12}}

The invoke’s input reaches the child’s implementations.context, and the output the child’s root computes reaches the parent’s onDone as event.output. The quote machine’s initial state is final, so the child is done in its initial macrostep; it reports to its parent once the parent’s own initial macrostep has settled and the parent has started it, and the parent takes onDone in a macrostep of its own, as in XState.

Rules

Creating a machine checks what implementations.inputs and implementations.outputs name, and refuses, with invalid_config, what could never be called or would contradict the config: an input that names no invoke, an input for an invoke whose config fixes one too, an output that names neither a final state nor the root, and an output for a state whose config fixes one too (What creation refuses). XState has nothing of the kind to check, since its functions sit in the config where they apply. An empty std::function, in implementations.inputs, implementations.outputs, implementations.context or the input of a spawn_child_action, is refused with invalid_config too, because calling it would abort in a build without exceptions.

const xstate::value_maker nothing =
    [](const xstate::action_args&) -> xstate::result<std::optional<boost::json::value>> {
    return std::optional<boost::json::value>();
};
const boost::json::value config = boost::json::parse(R"({
    "id": "feedback",
    "initial": "loading",
    "states": {
        "loading": {
            "invoke": {
                "id": "fetchUser",
                "src": "fetchUser",
                "input": {"userId": "42"}
            },
            "on": {"loaded": "closed", "cancel": "cancelled"}
        },
        "closed": {"type": "final"},
        "cancelled": {"type": "final", "output": "cancelled"}
    }
})");
xstate::implementations registry;
registry.actors.emplace("fetchUser", xstate::host_actor{});

xstate::implementations no_such_invoke = registry;
no_such_invoke.inputs.emplace("fetchUsers", nothing);
xstate::implementations input_twice = registry;
input_twice.inputs.emplace("fetchUser", nothing);
xstate::implementations not_final = registry;
not_final.outputs.emplace("feedback.loading", nothing);
xstate::implementations output_twice = registry;
output_twice.outputs.emplace("feedback.cancelled", nothing);
xstate::implementations final_and_root = registry;
final_and_root.outputs.emplace("feedback.closed", nothing);
final_and_root.outputs.emplace("feedback", nothing);

std::cout << "an input for no invoke: " << created(config, no_such_invoke) << '\n'
          << "an input the config fixes too: " << created(config, input_twice) << '\n'
          << "an output for a state not final: " << created(config, not_final) << '\n'
          << "an output the config fixes too: " << created(config, output_twice) << '\n'
          << "outputs for a final state and the root: " << created(config, final_and_root)
          << '\n';

It prints:

an input for no invoke: invalid_config
an input the config fixes too: invalid_config
an output for a state not final: invalid_config
an output the config fixes too: invalid_config
outputs for a final state and the root: created

created returns the error’s message, or created. A final state and the root may each have an output, fixed or computed, as the last line shows.

Once the machine runs, a computed input or output that returns an error fails the microstep that called it, as any implementation does: the macrostep settles with the snapshot it began from, its status error and its error the function’s (When an implementation fails). One that returns none leaves its member out (When a value is undefined).

Input and output cheatsheet

XState xstate, fixed xstate, computed

createActor(machine, { input })

create_actor(machine, {.input = ...})

initialTransition(machine, input)

initial_transition(machine, input), begin_initial(machine, input)

context: ({ input }) => ...

the config’s "context", which does not read the input

implementations.context

event.input in the initial macrostep

args.event.payload.if_contains("input")

invoke: { input }

the invoke’s "input"

implementations.inputs, under the invoke’s id

spawnChild(src, { id, input })

spawn_child_action::input

a final state’s output

the final state’s "output"

implementations.outputs, under the state’s id

the machine’s output

the root’s "output"

implementations.outputs, under the root’s id

snapshot.output

snapshot::output, none for undefined

event.output in onDone

args.event.payload.if_contains("output")

Pure transitions and the macrostep cursor

A pure transition function computes where a machine goes, and what it asks its caller to do, from a snapshot and an event. It starts no actor, executes no action and keeps nothing between calls: the same machine, snapshot and event always give the same result.

These functions are for a caller that keeps the snapshot itself: a server that stores each user’s snapshot between two requests, a test that checks where an event leads, a tool that shows what a machine can do next. Each needs the next step computed, not run, and holds the snapshot where it likes. XState offers them beside its actors, as initialTransition, transition and their relatives. Each call of one of XState’s that computes a step still constructs actors it never starts. One is an actor of the machine itself, whose construction resolves the machine’s initial macrostep whatever snapshot the call starts from: it runs the context function, the guards and every function a built-in action is written with, though no custom action, and constructs every child that macrostep invokes or spawns. The others are one for each child the step invokes or spawns, which the call puts in the snapshot’s children.

In xstate they are not an addition: they are the machine core. A machine is an immutable value and a snapshot a plain value the caller keeps; nothing in <webcpp/xstate.hpp> creates or runs an actor, executes a custom action, reads a clock or keeps state the caller does not hold: the snapshot, the cursor and the clock are values the caller owns. A call computes the step it is asked for and nothing else. A snapshot’s children are data, the id and the source of each child, and a step that invokes or spawns one returns an xstate.spawnChild action for its caller. The actor layer of Actors is built on them.

Every function that computes a step, transition and its relatives, is one primitive run to its end: the macrostep cursor, which xstate adds to XState. The cursor runs a macrostep one microstep per call, so its caller decides how much work an event may cost and sees each microstep on the way. The other functions ask about a snapshot and run no step. This section shows the functions first, then the cursor under them, then the clock that delayed events need.

xstate XState Returns

initial_transition(m, input)

initialTransition(m, input)

the initial snapshot and its actions

transition(m, s, e)

transition(m, s, e)

the snapshot an event leads to and its actions

get_initial_snapshot(m, input)

getInitialSnapshot(m, input)

the initial snapshot

get_next_snapshot(m, s, e)

getNextSnapshot(m, s, e)

the snapshot an event leads to

get_initial_microsteps(m, input)

getInitialMicrosteps(m, input)

each microstep of the initial macrostep

get_microsteps(m, s, e)

getMicrosteps(m, s, e)

each microstep of an event’s macrostep

can(m, s, e)

s.can(e)

whether the event selects a transition with a target or actions, as a result

get_next_transitions(m, s)

getNextTransitions(s)

every transition the snapshot could take

resolve_state(m, value, context)

m.resolveState({ value, context })

the snapshot of a state value, as a result

none

getStateNodes(node, value)

the nodes a state value names (The active state nodes)

begin_initial(m, input), begin(m, s, e)

none

a cursor over one macrostep

input is optional and null when left out. Where XState’s function takes a snapshot alone, xstate’s takes the machine too: a snapshot is plain data and does not point to its machine (The snapshot). The queries of XState’s snapshot are taught with states: snapshot::matches, and matches_state for two state values, XState’s snapshot.matches and matchesState (Matching a state value); snapshot::has_tag, XState’s snapshot.hasTag (Tags); and get_meta(machine, snapshot), XState’s snapshot.getMeta() (Meta and descriptions).

Computing the next state

The caller keeps the snapshot, and each event runs one macrostep from it. initial_transition(machine) returns the snapshot a machine starts in and the actions its initial macrostep returned. transition(machine, snapshot, event) returns the snapshot the event leads to and the actions of its macrostep: the caller keeps the snapshot for the next event and executes the actions. Both return a std::pair, which structured bindings take apart as JavaScript takes apart the [snapshot, actions] tuple of XState’s functions. types_of lists the types of the actions returned:

/** The types of `actions`, as a list: [a, b]. */
std::string types_of(const std::vector<xstate::action>& actions) {
    std::string listed = "[";
    for (const xstate::action& one : actions) {
        if (listed.size() > 1) {
            listed += ", ";
        }
        listed += one.type;
    }
    return listed + "]";
}

const boost::json::value config = boost::json::parse(R"({
    "initial": "pending",
    "states": {
        "pending": {"on": {"start": {"target": "started"}}},
        "started": {"entry": {"type": "doSomething"}}
    }
})");
const xstate::result<xstate::machine> machine = xstate::create_machine(config, {});
if (!machine.has_value()) {
    return 1;
}

const auto [initial_state, initial_actions] = xstate::initial_transition(*machine);
std::cout << "initial value: " << boost::json::serialize(initial_state.value) << '\n';
std::cout << "initial actions: " << types_of(initial_actions) << '\n';

const xstate::event start{.type = "start", .payload = {}};
const auto [next_state, actions] = xstate::transition(*machine, initial_state, start);
std::cout << "next value: " << boost::json::serialize(next_state.value) << '\n';
std::cout << "actions: " << types_of(actions) << '\n';

It prints:

initial value: "pending"
initial actions: []
next value: "started"
actions: [doSomething]

doSomething has no implementation, so it is a custom action: the functions return it and execute nothing (Actions). Stately’s page says the initial actions hold doSomething too, but pending has no entry action, and XState 5.33.2 returns none, as xstate does.

Neither the machine nor initial_state changes: each call returns a new snapshot. A snapshot can be kept, copied or stored, and transition called from it again gives the same result every time.

The event is an xstate::event, a type and a JSON payload, built in C++ or read with event_from_json from the flat JSON form XState’s actor.send takes, and the implementations read it as args.event (Events). The caller sends only the events that come from outside. Those the machine raises itself, a raised event or a done event, run inside the same macrostep (Actions, State done events), and a delayed event comes back from the caller’s clock, to be sent as any other (Delayed events and the simulated clock).

These functions run the macrostep to its end, however many microsteps it takes: a machine whose eventless transitions never settle never returns from them, as it never returns from XState’s. A caller that must bound the work of an event drives the cursor instead (The macrostep cursor).

Input

A machine whose initial context depends on an input takes the input as the last argument of initial_transition, get_initial_snapshot, get_initial_microsteps and begin_initial. XState writes that context as a function of the input in the config, context: ({ input }) => .... A function cannot be JSON, so xstate takes it as implementations.context, a context_maker that receives the input and returns the context (Input without an actor has the rest). name_of spells a status as XState does:

/** The name of a status, as XState spells it. */
std::string_view name_of(xstate::status status) {
    switch (status) {
        case xstate::status::active: return "active";
        case xstate::status::done: return "done";
        case xstate::status::error: return "error";
    }
    return "unknown";
}

xstate::implementations implementations;
implementations.context =
    [](const boost::json::value& input) -> xstate::result<boost::json::value> {
    const boost::json::object* given = input.if_object();
    const boost::json::value* count =
        given == nullptr ? nullptr : given->if_contains("initialCount");
    if (count == nullptr) {
        return xstate::failure<boost::json::value>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"count", *count}};
};
const boost::json::value config = boost::json::parse(R"({
    "initial": "pending",
    "states": {
        "pending": {"on": {"start": {"target": "started"}}},
        "started": {"entry": {"type": "doSomething"}}
    }
})");
const xstate::result<xstate::machine> machine = xstate::create_machine(config, implementations);
if (!machine.has_value()) {
    return 1;
}

const boost::json::value input = boost::json::object{{"initialCount", 0}};
const auto [initial_state, initial_actions] = xstate::initial_transition(*machine, input);
std::cout << "value: " << boost::json::serialize(initial_state.value) << '\n';
std::cout << "context: " << boost::json::serialize(initial_state.context) << '\n';
std::cout << "status: " << name_of(initial_state.status) << '\n';

const auto [without_input, no_actions] = xstate::initial_transition(*machine);
std::cout << "without input: " << name_of(without_input.status) << ", context "
          << boost::json::serialize(without_input.context) << '\n';

It prints:

value: "pending"
context: {"count":0}
status: active
without input: error, context {}

Without an input, the context function receives null and returns an error, and initial_transition returns the snapshot the machine was about to start from, with status error and an empty context. XState reaches the same snapshot another way: its function reads a member of undefined and throws, and initialTransition catches the error into the snapshot it returns.

One case differs: xstate refuses a context function that returns anything but an object, with implementation_failed and the same snapshot, where XState merges whatever its function returns into {} with Object.assign, so a number or null starts the machine with an empty context (A context that is not an object).

The snapshot alone

get_initial_snapshot and get_next_snapshot return the snapshot without the actions. XState marks its getInitialSnapshot and getNextSnapshot deprecated in favour of initialTransition and transition; xstate keeps both, as the short form for a caller that has no use for the actions:

const boost::json::value config = boost::json::parse(R"({
    "id": "light",
    "initial": "green",
    "states": {
        "green": {"on": {"TIMER": "yellow"}},
        "yellow": {"on": {"TIMER": "red"}},
        "red": {"on": {"TIMER": "green"}}
    }
})");
const xstate::result<xstate::machine> light = xstate::create_machine(config, {});
if (!light.has_value()) {
    return 1;
}
const xstate::event timer{.type = "TIMER", .payload = {}};
const xstate::event unknown{.type = "UNKNOWN", .payload = {}};

const xstate::snapshot green = xstate::get_initial_snapshot(*light);
const xstate::snapshot yellow = xstate::get_next_snapshot(*light, green, timer);
const xstate::snapshot red = xstate::get_next_snapshot(*light, yellow, timer);
const xstate::snapshot still_red = xstate::get_next_snapshot(*light, red, unknown);

std::cout << boost::json::serialize(green.value) << '\n';
std::cout << boost::json::serialize(yellow.value) << '\n';
std::cout << boost::json::serialize(red.value) << '\n';
std::cout << boost::json::serialize(still_red.value) << '\n';

It prints:

"green"
"yellow"
"red"
"red"

An event no transition takes, UNKNOWN here, is not an error: its macrostep settles at once and the snapshot is unchanged. can says beforehand whether an event would do anything.

Actions

An action is something the machine asks to happen: send an email, change the context, raise an event. The pure functions resolve each action and return it, in the order XState returns it; they execute none. What comes back depends on what the action’s name stands for:

  • A name the implementations do not hold is a custom action. It comes back as the config names it: its type, and its params from the config, null when the config gives none, and builtin false, even when the config gives it the type of a built-in action, such as xstate.raise. Executing it is the caller’s work (Custom actions).

  • An assign merges its update into the context. It never comes back.

  • Every other built-in action comes back with its params resolved and builtin set: a raise as xstate.raise, a log as xstate.log, a cancel as xstate.cancel, and so on. What concerns the machine itself is done already: a raise without a delay has been processed inside the macrostep.

Here the transition on processUser names a custom action, an assign and a raise:

const xstate::assigner count_user =
    [](const xstate::action_args& args) -> xstate::result<boost::json::object> {
    const boost::json::object* context = args.context.if_object();
    const boost::json::value* users =
        context == nullptr ? nullptr : context->if_contains("users");
    if (users == nullptr || !users->is_int64()) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"users", users->get_int64() + 1}};
};
const xstate::event_maker started =
    [](const xstate::action_args&) -> xstate::result<xstate::event> {
    return xstate::event{.type = "STARTED", .payload = {}};
};
xstate::implementations implementations;
implementations.actions.emplace("countUser", xstate::assign_action{.assignment = count_user});
implementations.actions.emplace(
    "announce",
    xstate::raise_action{.event = started, .id = std::nullopt, .delay = std::nullopt});

const boost::json::value config = boost::json::parse(R"({
    "initial": "idle",
    "context": {"users": 0},
    "states": {
        "idle": {
            "on": {
                "processUser": {
                    "target": "processing",
                    "actions": [
                        {"type": "sendEmail", "params": {"subject": "Processing started"}},
                        "countUser",
                        "announce"
                    ]
                }
            }
        },
        "processing": {"on": {"STARTED": "started"}},
        "started": {}
    }
})");
const xstate::result<xstate::machine> machine = xstate::create_machine(config, implementations);
if (!machine.has_value()) {
    return 1;
}

const xstate::snapshot idle = xstate::get_initial_snapshot(*machine);
const xstate::event process_user{
    .type = "processUser",
    .payload = {{"userId", "123"}, {"email", "user@example.com"}},
};
const auto [next_state, actions] = xstate::transition(*machine, idle, process_user);
for (const xstate::action& action : actions) {
    std::cout << action.type << ' ' << boost::json::serialize(action.params) << '\n';
}
std::cout << "value: " << boost::json::serialize(next_state.value) << '\n';
std::cout << "context: " << boost::json::serialize(next_state.context) << '\n';

It prints:

sendEmail {"subject":"Processing started"}
xstate.raise {"event":{"type":"STARTED"}}
value: "started"
context: {"users":1}

transition ran the whole macrostep: processUser took idle to processing, announce raised STARTED, and STARTED took processing to started. The raise comes back so the caller sees it, not so the caller sends STARTED again. countUser changed the context and is not in the list.

What the caller does with the built-in actions it receives:

  • An xstate.raise with a delay in its params, and an xstate.cancel, are for the caller’s clock (Delayed events and the simulated clock).

  • xstate.spawnChild, xstate.stopChild, xstate.sendTo and xstate.emit are for whatever runs the machine’s children and its listeners. The actor layer does that (Actors).

  • An xstate.log holds the value to log.

On Stately’s page, a custom action’s params are a function of the context and the event: sendEmail reads event.email. A function cannot be JSON, so xstate’s params are what the config fixes, and what comes from the event is read where the event is: each microstep reports its event beside its actions (Microsteps). Actions covers every built-in action, and Functions become names the reason for named implementations.

Microsteps

One event can move a machine more than once. The transitions the event selects run first. Then XState’s loop runs one microstep at a time: the eventless (always) transitions that are enabled, or, when none is, the next event the machine raised, until no eventless transition is enabled and no raised event is queued. Each of those steps is a microstep; together they are a macrostep (Core concepts). transition returns where the macrostep ends; get_microsteps and get_initial_microsteps return every microstep on the way.

The microsteps of an event

get_microsteps(machine, snapshot, event) returns each microstep of the macrostep an event runs, as XState’s getMicrosteps does. Here TRIGGER moves the machine, raises an event, and leads to an eventless transition:

/** The types of `actions`, as a list: [a, b]. */
std::string types_of(const std::vector<xstate::action>& actions) {
    std::string listed = "[";
    for (const xstate::action& one : actions) {
        if (listed.size() > 1) {
            listed += ", ";
        }
        listed += one.type;
    }
    return listed + "]";
}

const xstate::event_maker raised =
    [](const xstate::action_args&) -> xstate::result<xstate::event> {
    return xstate::event{.type = "RAISED", .payload = {}};
};
xstate::implementations implementations;
implementations.actions.emplace(
    "raiseRaised",
    xstate::raise_action{.event = raised, .id = std::nullopt, .delay = std::nullopt});

const boost::json::value config = boost::json::parse(R"({
    "initial": "first",
    "states": {
        "first": {
            "on": {"TRIGGER": {"target": "second", "actions": "raiseRaised"}}
        },
        "second": {"on": {"RAISED": "third"}},
        "third": {"always": "fourth"},
        "fourth": {}
    }
})");
const xstate::result<xstate::machine> machine = xstate::create_machine(config, implementations);
if (!machine.has_value()) {
    return 1;
}

const xstate::snapshot initial_state = xstate::get_initial_snapshot(*machine);
const xstate::event trigger{.type = "TRIGGER", .payload = {}};
for (const xstate::microstep& micro :
     xstate::get_microsteps(*machine, initial_state, trigger)) {
    std::cout << boost::json::serialize(micro.snapshot.value) << ' ' << types_of(micro.actions)
              << '\n';
}

It prints:

"second" [xstate.raise]
"third" []
"fourth" []

TRIGGER takes first to second and raises RAISED, which takes second to third, whose eventless transition takes it to fourth: three microsteps, one macrostep. XState’s raise(...) is a call written in the config; xstate names it, raiseRaised, and registers a raise_action under that name.

XState’s microstep is a pair, [snapshot, actions]. xstate’s microstep has four members:

event

the event it ran for: the external one, a raised one, or, for an eventless transition, the event of the microstep before it.

transitions

the transitions it took, as pointers into the machine, valid while any copy of the machine lives: an empty list when the event selected none, and std::nullopt for the initial microstep, which reports none, for a microstep that failed while it ran, and for a next() on a cursor that is done.

actions

the actions it returned, in order.

snapshot

the snapshot it left.

The microsteps of the initial state

get_initial_microsteps(machine) returns each microstep of the initial macrostep, as XState’s getInitialMicrosteps does. Here the initial state has an entry action and an eventless transition:

/** The types of `actions`, as a list: [a, b]. */
std::string types_of(const std::vector<xstate::action>& actions) {
    std::string listed = "[";
    for (const xstate::action& one : actions) {
        if (listed.size() > 1) {
            listed += ", ";
        }
        listed += one.type;
    }
    return listed + "]";
}

const boost::json::value config = boost::json::parse(R"({
    "initial": "a",
    "states": {
        "a": {
            "entry": "enterA",
            "always": {"target": "b", "actions": "aToB"}
        },
        "b": {"entry": "enterB"}
    }
})");
const xstate::result<xstate::machine> machine = xstate::create_machine(config, {});
if (!machine.has_value()) {
    return 1;
}

const std::vector<xstate::microstep> microsteps = xstate::get_initial_microsteps(*machine);
for (const xstate::microstep& micro : microsteps) {
    std::cout << boost::json::serialize(micro.snapshot.value) << ' ' << types_of(micro.actions)
              << '\n';
}

It prints:

"a" [enterA]
"b" [aToB, enterB]

The first microstep enters the initial state and returns its entry action. The second is the eventless transition of a: its action, then the entry of b. Stately’s page writes these actions as inline functions; here they are names with no implementation, which come back as custom actions, as they come back from XState’s getInitialMicrosteps. A machine with an input takes it as the second argument: get_initial_microsteps(machine, input).

Asking about a snapshot

Whether an event would do anything

can(machine, snapshot, event) says whether the event would do anything from the snapshot: whether it selects a transition that has a target or actions. It evaluates guards, as selecting a transition does, and executes nothing. It is XState’s snapshot.can(event):

const xstate::predicate has_text = [](const xstate::action_args& args) -> xstate::result<bool> {
    const boost::json::object* context = args.context.if_object();
    const boost::json::value* text =
        context == nullptr ? nullptr : context->if_contains("text");
    if (text == nullptr || !text->is_string()) {
        return xstate::failure<bool>(xstate::errc::implementation_failed);
    }
    return !text->get_string().empty();
};
const xstate::assigner keep =
    [](const xstate::action_args& args) -> xstate::result<boost::json::object> {
    const boost::json::value* text = args.event.payload.if_contains("text");
    if (text == nullptr) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"text", *text}};
};
xstate::implementations implementations;
implementations.guards.emplace("hasText", has_text);
implementations.actions.emplace("keep", xstate::assign_action{.assignment = keep});

const boost::json::value config = boost::json::parse(R"({
    "id": "form",
    "initial": "editing",
    "context": {"text": ""},
    "states": {
        "editing": {
            "on": {
                "SUBMIT": {"target": "submitted", "guard": "hasText"},
                "TYPE": {"actions": "keep"},
                "IGNORE": {}
            }
        },
        "submitted": {"type": "final"}
    }
})");
const xstate::result<xstate::machine> form = xstate::create_machine(config, implementations);
if (!form.has_value()) {
    return 1;
}

const xstate::snapshot editing = xstate::get_initial_snapshot(*form);
for (const std::string_view type : {"SUBMIT", "TYPE", "IGNORE", "UNKNOWN"}) {
    const xstate::result<bool> able =
        xstate::can(*form, editing, xstate::event{.type = std::string(type), .payload = {}});
    if (!able.has_value()) {
        return 1;
    }
    std::cout << type << ": " << (*able ? "true" : "false") << '\n';
}

const xstate::event type_hi{.type = "TYPE", .payload = {{"text", "hi"}}};
const xstate::snapshot typed = xstate::get_next_snapshot(*form, editing, type_hi);
const xstate::result<bool> submit =
    xstate::can(*form, typed, xstate::event{.type = "SUBMIT", .payload = {}});
if (!submit.has_value()) {
    return 1;
}
std::cout << "after TYPE, SUBMIT: " << (*submit ? "true" : "false") << '\n';

It prints:

SUBMIT: false
TYPE: true
IGNORE: false
UNKNOWN: false
after TYPE, SUBMIT: true

SUBMIT is guarded by hasText, false while the text is empty. TYPE has an action, so it does something without a target. IGNORE selects a transition that has neither a target nor actions, which changes nothing, so can is false although a transition is selected. UNKNOWN selects nothing.

can returns a result<bool>: a guard is an implementation and may fail, and the guard’s error comes back in place of an answer.

Every transition a snapshot could take

get_next_transitions(machine, snapshot) lists every transition the snapshot could take, without evaluating a guard: for each active atomic state, in the order of nodes, its own transitions and then those of each ancestor not listed yet; within a state, its event transitions in the order of the node’s transitions, which is XState’s, and then its eventless ones. It is XState’s getNextTransitions(snapshot), for a tool that shows what can happen next:

/** The ids of a transition's targets, or "(targetless)". */
std::string targets_of(const xstate::machine& owner, const xstate::transition_definition& one) {
    if (!one.target.has_value()) {
        return "(targetless)";
    }
    std::string listed;
    for (const std::size_t node : *one.target) {
        if (!listed.empty()) {
            listed += ", ";
        }
        listed += owner.node(node).id;
    }
    return listed;
}

const boost::json::value config = boost::json::parse(R"({
    "id": "form",
    "initial": "editing",
    "on": {"RESET": ".editing"},
    "states": {
        "editing": {
            "on": {
                "SUBMIT": {"target": "submitted", "guard": "hasText"},
                "TYPE": {"actions": "keep"}
            },
            "always": {"target": "submitted", "guard": "isFull"}
        },
        "submitted": {"type": "final"}
    }
})");
xstate::implementations implementations;
implementations.guards.emplace("hasText", [](const xstate::action_args&) { return true; });
implementations.guards.emplace("isFull", [](const xstate::action_args&) { return false; });
const xstate::result<xstate::machine> form = xstate::create_machine(config, implementations);
if (!form.has_value()) {
    return 1;
}

const xstate::snapshot editing = xstate::get_initial_snapshot(*form);
for (const xstate::transition_definition* one : xstate::get_next_transitions(*form, editing)) {
    std::cout << '"' << one->event_type << "\" " << form->node(one->source).id << " -> "
              << targets_of(*form, *one) << '\n';
}

It prints:

"SUBMIT" form.editing -> form.submitted
"TYPE" form.editing -> (targetless)
"" form.editing -> form.submitted
"RESET" form -> form.editing

An eventless transition has the empty descriptor "", and a delayed one the type of its delayed event, xstate.after.<delay>.<state id> (Delayed transitions). A targetless transition has no target (std::nullopt), which is not the same as an empty list of targets. Both guarded transitions are listed: whether a guard holds is the question can answers. Each element points into the machine, like a microstep’s transitions.

The active state nodes

A snapshot lists its active state nodes in nodes, as indexes into the machine; machine::node(index) reads one, with its id, its key, its type and the rest of its definition (State nodes and ids):

/** Prints a snapshot's state value, then the id of each active node, in order. */
void print_nodes(const xstate::machine& owner, const xstate::snapshot& of) {
    std::cout << boost::json::serialize(of.value) << '\n';
    for (const std::size_t node : of.nodes) {
        std::cout << ' ' << owner.node(node).id;
    }
    std::cout << '\n';
}

const boost::json::value config = boost::json::parse(R"({
    "id": "player",
    "initial": "on",
    "states": {
        "off": {},
        "on": {
            "type": "parallel",
            "on": {"OFF": "#player.off"},
            "states": {
                "track": {
                    "initial": "paused",
                    "states": {
                        "paused": {"on": {"PLAY": "playing"}},
                        "playing": {}
                    }
                },
                "volume": {
                    "initial": "normal",
                    "states": {
                        "normal": {"on": {"MUTE": "muted"}},
                        "muted": {}
                    }
                }
            }
        }
    }
})");
const xstate::result<xstate::machine> player = xstate::create_machine(config, {});
if (!player.has_value()) {
    return 1;
}

const xstate::snapshot paused = xstate::get_initial_snapshot(*player);
print_nodes(*player, paused);
const xstate::snapshot playing =
    xstate::get_next_snapshot(*player, paused, xstate::event{.type = "PLAY", .payload = {}});
print_nodes(*player, playing);

It prints:

{"on":{"track":"paused","volume":"normal"}}
 player player.on player.on.track player.on.track.paused player.on.volume player.on.volume.normal
{"on":{"track":"playing","volume":"normal"}}
 player player.on player.on.track player.on.volume player.on.volume.normal player.on.track.playing

The order is XState’s, the order of its snapshot’s internal _nodes (The snapshot): a node that stays active keeps its place, an exited one leaves, and an entered one is appended, so after PLAY the node playing comes last. A microstep that leaves the machine done lists the nodes in reverse document order instead, as XState’s does. The order decides which eventless transition a parallel region takes first. It also orders a state value’s keys, all but those that are array indexes: XState’s value is a JavaScript object, which enumerates such keys first, by number, and xstate writes them first too.

XState also exports getStateNodes(stateNode, stateValue), the nodes a state value names below a node, as it names them: no initial state the value leaves out, and a node repeated for each level of an object value. xstate has no counterpart. The nearest is resolve_state (Persisted state): its snapshot’s nodes are every node the value implies, completed with the initial states it leaves out, each listed once. XState’s resolveState computes the same nodes, in the same order: it completes the value first, then takes getAllStateNodes(getStateNodes(root, value)) of the completed value. Both read an object value’s keys in the order JavaScript enumerates them, array indexes first, so they agree whatever order the value writes its keys in.

Persisted state

A snapshot is a value, so a server can keep it between requests: persist it, and turn it back into a snapshot when the next event comes. resolve_state(machine, value, context) is XState’s machine.resolveState({ value, context }): it completes a state value to every node it implies and returns that snapshot, with status done when the value is a final state and active otherwise. Here the traffic light shown above (The snapshot alone), given a context, is persisted in yellow, restored, and sent TIMER:

const boost::json::value config = boost::json::parse(R"({
    "id": "light",
    "initial": "green",
    "context": {"cycles": 0},
    "states": {
        "green": {"on": {"TIMER": "yellow"}},
        "yellow": {"on": {"TIMER": "red"}},
        "red": {"on": {"TIMER": "green"}}
    }
})");
const xstate::result<xstate::machine> machine = xstate::create_machine(config, {});
if (!machine.has_value()) {
    return 1;
}
const xstate::event timer{.type = "TIMER", .payload = {}};
const xstate::snapshot current_state =
    xstate::get_next_snapshot(*machine, xstate::get_initial_snapshot(*machine), timer);

const std::string state_to_persist = boost::json::serialize(boost::json::object{
    {"value", current_state.value},
    {"context", current_state.context},
});

/** Later, perhaps in another process, the persisted text is read back. */
boost::system::error_code malformed;
const boost::json::value persisted = boost::json::parse(state_to_persist, malformed);
const boost::json::object* fields = persisted.if_object();
if (malformed || fields == nullptr || !fields->contains("value") ||
    !fields->contains("context")) {
    return 1;
}
const xstate::result<xstate::snapshot> restored_state =
    xstate::resolve_state(*machine, fields->at("value"), fields->at("context"));
if (!restored_state.has_value()) {
    return 1;
}
const auto [next_state, actions] = xstate::transition(*machine, *restored_state, timer);
std::cout << "restored: " << boost::json::serialize(restored_state->value) << '\n';
std::cout << "context: " << boost::json::serialize(restored_state->context) << '\n';
std::cout << "next: " << boost::json::serialize(next_state.value) << '\n';

It prints:

restored: "yellow"
context: {"cycles":0}
next: "red"

resolve_state restores the value and the context, and nothing else: the status comes from the value, done or active, never error, and a done machine’s output is not restored. XState’s resolveState also reads status, output, error and historyValue from what it is given, so its round trip of a done snapshot keeps the output and xstate’s does not. A value that names a state the machine does not have is refused with unknown_state. What history states remember is not restored, and neither are the children: the snapshot starts without them.

XState’s resolveState restores what history states remember only from a live historyValue, whose members are state node objects: from what JSON.stringify made of them, a later transition to the history state enters a plain object in place of the node, and the snapshot is corrupt. XState’s actor persistence, getPersistedSnapshot and createActor with a snapshot, restores it from JSON by node id; xstate has no counterpart (What is not ported), so a caller that needs what history states remember keeps the snapshot itself, in memory.

The macrostep cursor

The macrostep cursor runs one macrostep a microstep at a time, under its caller’s control. It is xstate’s one primitive, and XState has no counterpart: XState runs a macrostep to its end inside one call, and its loop has no bound but the config’s options.maxIterations, infinite unless the config sets it (A macrostep that never settles). Without it, a machine whose eventless transitions or raised events lead back to each other never returns from transition.

That is acceptable when the machine is code the program was written with, whose author can find such a cycle before the program ships. A program that runs machines it did not write cannot. xstate loads a machine from JSON when the program runs: a user may have written it, Stately Studio exported it, a request carried it. A server runs it inside a request it must answer within a budget, and one cycle in that machine, by mistake or by intent, would hold the server’s thread for good. So the program pays for each microstep with fuel, and stops the macrostep when its budget is spent.

A cursor, macrostep, holds one macrostep in progress. Each call of next() runs exactly one microstep and reports it, with whether that microstep settled the macrostep. The caller decides whether to call next() again, and so how much an event may cost: one unit of fuel per microstep is the natural measure. The actor layer pays it, plus one unit for each message an actor sends and each timer it arms (Fuel, parking and resume). Every function above that computes a step is this cursor run to its end without a bound, so their results are the cursor’s.

One microstep at a time

begin(machine, snapshot, event) returns the cursor of the macrostep the event runs from the snapshot. With this machine, where charge is a raise_action of CHARGED, CHECKOUT takes three microsteps:

const boost::json::value config = boost::json::parse(R"({
    "id": "order",
    "initial": "idle",
    "states": {
        "idle": {"on": {"CHECKOUT": {"target": "validating", "actions": "track"}}},
        "validating": {"always": "charging"},
        "charging": {"entry": "charge", "on": {"CHARGED": "confirmed"}},
        "confirmed": {"entry": "sendReceipt"}
    }
})");

Two helpers spell what a microstep returned and took. A transition names its source and its targets by their index in the machine, and machine::node reads each:

/** The types of `actions`, as a list: [a, b]. */
std::string types_of(const std::vector<xstate::action>& actions) {
    std::string listed = "[";
    for (const xstate::action& one : actions) {
        if (listed.size() > 1) {
            listed += ", ";
        }
        listed += one.type;
    }
    return listed + "]";
}

/** The transitions a microstep took, as source -> targets, or "none". */
std::string transitions_of(const xstate::machine& owner, const xstate::microstep& micro) {
    if (!micro.transitions.has_value() || micro.transitions->empty()) {
        return "none";
    }
    std::string listed;
    for (const xstate::transition_definition* one : *micro.transitions) {
        if (!listed.empty()) {
            listed += ", ";
        }
        listed += owner.node(one->source).key + " ->";
        for (const std::size_t target : one->target.value_or(std::vector<std::size_t>{})) {
            listed += ' ' + owner.node(target).key;
        }
    }
    return listed;
}

The cursor runs the microsteps one per call of next():

xstate::macrostep cursor =
    xstate::begin(*order, idle, xstate::event{.type = "CHECKOUT", .payload = {}});
std::size_t count = 0;
while (!cursor.done()) {
    const xstate::progress made = cursor.next();
    const xstate::microstep& micro = made.step;
    std::cout << ++count << ": " << micro.event.type << ", " << transitions_of(*order, micro)
              << ", " << types_of(micro.actions) << ", "
              << boost::json::serialize(micro.snapshot.value)
              << (made.settled ? ", settled" : "") << '\n';
}
const xstate::macrostep_result& settled = cursor.result();
std::cout << "result: " << boost::json::serialize(settled.snapshot.value) << ", "
          << types_of(settled.actions) << ", " << settled.microsteps << " microsteps\n";

It prints:

1: CHECKOUT, idle -> validating, [track], "validating"
2: CHECKOUT, validating -> charging, [xstate.raise], "charging"
3: CHARGED, charging -> confirmed, [sendReceipt], "confirmed", settled
result: "confirmed", [track, xstate.raise, sendReceipt], 3 microsteps

Each next() returns a progress:

step

the microstep it ran.

settled

true on the microstep after which XState’s loop would stop, no eventless transition being enabled and no raised event queued, or the machine no longer active; false on every other.

resolved_before_failure

the actions a failing microstep resolved before it failed (When an implementation fails).

The second microstep is the eventless transition of validating. It reports the event of the microstep before it, CHECKOUT, as XState does. The third ran CHARGED, which charging raised on entry.

Between two calls, the caller can read what the microstep did: the context its assigns left, the transitions it took, the actions it returned. The microstep that settles leaves the snapshot the macrostep settles in, so a caller that acts after each microstep takes its new stable state from that microstep’s snapshot.

Once done() is true, result() holds the macrostep_result: the settled snapshot, every action of every microstep in order, and the number of microsteps. Two more accessors read a cursor while it runs: current(), the snapshot the last microstep left, which is not a stable state until the macrostep settles, and began_from(), the snapshot the macrostep started from. A next() on a cursor that is done runs nothing and reports settled again. An event that selects no transition settles on the first next(), with the snapshot unchanged.

The initial macrostep

begin_initial(machine, input) returns the cursor of the initial macrostep: the microstep that enters the initial states, then XState’s loop for the event xstate.init, which carries the input. It prints with the helpers types_of and transitions_of shown above (One microstep at a time):

const xstate::context_maker from_input =
    [](const boost::json::value& input) -> xstate::result<boost::json::value> {
    const boost::json::object* given = input.if_object();
    const boost::json::value* user = given == nullptr ? nullptr : given->if_contains("user");
    return boost::json::object{{"user", user == nullptr ? boost::json::value() : *user}};
};
const xstate::predicate has_user = [](const xstate::action_args& args) -> xstate::result<bool> {
    const boost::json::object* context = args.context.if_object();
    const boost::json::value* user =
        context == nullptr ? nullptr : context->if_contains("user");
    return user != nullptr && !user->is_null();
};
xstate::implementations implementations;
implementations.context = from_input;
implementations.guards.emplace("hasUser", has_user);

const boost::json::value config = boost::json::parse(R"({
    "id": "session",
    "initial": "checking",
    "states": {
        "checking": {
            "always": [
                {"target": "signedIn", "guard": "hasUser"},
                {"target": "signedOut"}
            ]
        },
        "signedIn": {"entry": "welcome"},
        "signedOut": {}
    }
})");
const xstate::result<xstate::machine> session = xstate::create_machine(config, implementations);
if (!session.has_value()) {
    return 1;
}

const boost::json::value input = boost::json::object{{"user", "ana"}};
xstate::macrostep cursor = xstate::begin_initial(*session, input);
while (!cursor.done()) {
    const xstate::progress made = cursor.next();
    const xstate::microstep& micro = made.step;
    std::cout << boost::json::serialize(xstate::to_json(micro.event)) << ", "
              << transitions_of(*session, micro) << ", " << types_of(micro.actions) << ", "
              << boost::json::serialize(micro.snapshot.value)
              << (made.settled ? ", settled" : "") << '\n';
}
std::cout << "context: " << boost::json::serialize(cursor.result().snapshot.context) << '\n';

It prints:

{"type":"xstate.init","input":{"user":"ana"}}, none, [], "checking"
{"type":"xstate.init","input":{"user":"ana"}}, checking -> signedIn, [welcome], "signedIn", settled
context: {"user":"ana"}

begin_initial computes the context from the input before the first microstep, with implementations.context. A context that cannot be computed fails the macrostep on its first next(), as in Input. The first microstep reports no transitions, as XState reports none for it. The second is the eventless transition of checking, whose guard read the context; it reports xstate.init still. With a null input, xstate’s xstate.init has no input member, where XState’s has one that is undefined.

Fuel

Fuel is the budget a caller gives a macrostep: the caller pays for each microstep before it calls next(), and stops when the budget is spent. A cursor is a value, so stopping loses nothing. The caller keeps the cursor and calls next() again later, in the same request or in a later one the same process serves: a cursor holds its machine and points into it, so it lives in memory and is not written out as a snapshot is (Persisted state). It resumes exactly where it stopped, with the events the machine raised still queued. A copy is independent of the original and resumes from the same point. This function pays one unit of fuel per microstep:

/** Runs at most `fuel` microsteps of `cursor`, one unit each; returns the units spent. */
std::size_t run(xstate::macrostep& cursor, std::size_t fuel) {
    std::size_t spent = 0;
    while (!cursor.done() && spent < fuel) {
        cursor.next();
        ++spent;
    }
    return spent;
}

Two requests of two units each run the CHECKOUT macrostep shown above (One microstep at a time), and a copy taken between them runs too:

xstate::macrostep cursor =
    xstate::begin(*order, idle, xstate::event{.type = "CHECKOUT", .payload = {}});

const std::size_t first = run(cursor, 2);
std::cout << "first request: " << first << " microsteps, done " << cursor.done() << ", at "
          << boost::json::serialize(cursor.current().value) << '\n';

xstate::macrostep copy = cursor;

const std::size_t second = run(cursor, 2);
std::cout << "second request: " << second << " microstep, done " << cursor.done() << ", at "
          << boost::json::serialize(cursor.result().snapshot.value) << '\n';

run(copy, 2);
std::cout << "the copy: done " << copy.done() << ", at "
          << boost::json::serialize(copy.result().snapshot.value) << ", "
          << copy.result().microsteps << " microsteps\n";

It prints:

first request: 2 microsteps, done false, at "charging"
second request: 1 microstep, done true, at "confirmed"
the copy: done true, at "confirmed", 3 microsteps

The first request runs out of fuel after two microsteps, in charging, with CHARGED raised and queued. The second runs the one microstep left. The copy settles in the same snapshot after the same three microsteps: result().microsteps counts the macrostep’s microsteps, wherever they ran.

While a macrostep has not settled, its current() snapshot is not a stable state; the snapshot it began from is still the machine’s state for anything else the caller does. An initial macrostep has no such state: its began_from() is the snapshot before the machine started, whose nodes hold the root alone.

One unit a microstep is this example’s measure, not a rule: a caller may charge each microstep by what it costs, the actions it returns to execute among them.

Out of fuel in a cycle

A macrostep never settles when its eventless transitions keep enabling each other, or its raised events keep raising others: no microstep is the last (A macrostep that never settles). With the cursor, the caller sees it as a cursor still not done when its fuel is spent. Here SPIN enters left, whose always leads to right, whose always leads back to left, and the caller gives the macrostep 1000 units of fuel:

const boost::json::value config = boost::json::parse(R"({
    "id": "spinner",
    "initial": "idle",
    "states": {
        "idle": {"on": {"SPIN": "left"}},
        "left": {"always": "right"},
        "right": {"always": "left"}
    }
})");
const xstate::result<xstate::machine> spinner = xstate::create_machine(config, {});
if (!spinner.has_value()) {
    return 1;
}
const xstate::snapshot idle = xstate::get_initial_snapshot(*spinner);

constexpr std::size_t fuel = 1000;
xstate::macrostep cursor =
    xstate::begin(*spinner, idle, xstate::event{.type = "SPIN", .payload = {}});
std::size_t spent = 0;
while (!cursor.done() && spent < fuel) {
    cursor.next();
    ++spent;
}
if (!cursor.done()) {
    std::cout << "out of fuel after " << spent << " microsteps, at "
              << boost::json::serialize(cursor.current().value) << '\n';
    std::cout << "the stable state is still "
              << boost::json::serialize(cursor.began_from().value) << '\n';
}

It prints:

out of fuel after 1000 microsteps, at "right"
the stable state is still "idle"

The cursor does not stop on its own: no microstep settles, and the caller’s fuel is the only bound. When it is spent, current() is a snapshot inside the cycle, which is not a stable state, and began_from() is the stable state the event found. What happens then is the caller’s choice: drop the event and keep that stable state, as here, report an error, or resume later with more fuel. The actor layer parks the actor until resume() gives it more (Fuel, parking and resume). XState’s transition never returns on SPIN; given an options.maxIterations of 1000, it throws Infinite loop detected instead, and its caller keeps the snapshot it passed. Where xstate differs compares the two bounds.

Executing actions as they come

A caller that executes actions microstep by microstep reads each progress.step.actions; one that executes them once the macrostep settles reads result().actions, which gathers them in order. Each action is executed from one or the other, never both, or it runs twice.

XState’s actor does both, by kind. It runs a custom action or a log as it resolves it, and holds what every other built-in action does, an emit, a send, a delayed raise, a cancel, the start of a spawned child or the stop of a started one, until the macrostep ends, then runs those in order. A raise without a delay leaves nothing to run: the macrostep has processed its event already (Actions). A stop does part of its work at once: it releases the systemIds of the child’s family, and it stops a child that has not started, so a child that a macrostep spawns and then stops never starts: its held start finds it stopped and does nothing.

The actions of an initial macrostep wait until the actor starts, and its start orders them differently. It first starts the children the settled snapshot lists whose own snapshot is active, in the order of its children, then runs the custom actions and the logs, and then what the other built-in actions do, each in the order the macrostep returned them. A child born done or failed is not active: it starts at its xstate.spawnChild, with the other built-in actions. An active child of an initial macrostep thus starts before the custom actions of that macrostep run, where a child of any later macrostep starts after them.

A caller that acts as XState’s actor executes the custom actions and the logs from each progress.step.actions, and the other built-in actions from result().actions once the macrostep has settled, unless it failed (When an implementation fails). Of those it skips a raise without a delay, and both the xstate.spawnChild and the xstate.stopChild of a child that the same macrostep spawned and stopped. Of an initial macrostep it executes nothing until it starts the machine, and then executes them as that start does: it starts first the listed children whose own snapshot is active, and does not execute their xstate.spawnChild again, then executes the custom actions and the logs, then the other built-in actions, among them the xstate.spawnChild of a child born done or failed.

One kind of action is in result().actions only: when a macrostep settles with the machine not active, done or still in the error it began from, it returns an xstate.stopChild for each child left, after every microstep’s actions and in no microstep. A macrostep that ends in a new error returns none: one an implementation fails, or one whose event is an actor’s error that no transition handles. A caller that executes microstep by microstep executes the stops once the macrostep has settled.

One more rule concerns a send to a child that the same microstep invokes. An xstate.sendTo that names, by its bare id, an invoke of the state being entered may come back before that invoke’s xstate.spawnChild, since a state’s entry actions run before its invokes spawn their children. It carries bound_at: how many of its microstep’s actions come before the point where XState binds it to the child, the end of that state’s entry actions, spawns and initial actions (XState’s retryResolveSendTo). Its target is the child that id names once the caller has executed that many of the microstep’s actions. The actor layer binds it there, and fails with unknown_target when no child has that id by then (action::bound_at, A sendTo whose child is gone).

When an implementation fails

An implementation returns a result, and when it returns an error the microstep fails. That next() settles the macrostep at once, and the snapshot is the one the macrostep began from, with status error and the implementation’s error code in error. XState’s pure transition throws instead; its actor keeps the snapshot from before the macrostep, with status error. xstate throws no exception, and a failure returned as a value lands in the snapshot the caller receives, where it cannot be missed (Failures are values). The machine below returns custom actions alone. A cursor loop that executes them as they come does with them what XState’s actor does, and prints what an actor of XState prints for the same machine (name_of is the helper of the input example):

/** Executes a custom action, the only kind this machine returns for its caller to run. */
void execute(const xstate::action& action) {
    if (action.builtin) {
        return;
    }
    std::cout << "ran " << action.type << '\n';
}

const xstate::assigner count_attempt =
    [](const xstate::action_args& args) -> xstate::result<boost::json::object> {
    const boost::json::object* context = args.context.if_object();
    const boost::json::value* attempts =
        context == nullptr ? nullptr : context->if_contains("attempts");
    if (attempts == nullptr || !attempts->is_int64()) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"attempts", attempts->get_int64() + 1}};
};
const xstate::assigner fail =
    [](const xstate::action_args&) -> xstate::result<boost::json::object> {
    return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
};
xstate::implementations implementations;
implementations.actions.emplace("countAttempt",
                                xstate::assign_action{.assignment = count_attempt});
implementations.actions.emplace("fail", xstate::assign_action{.assignment = fail});

const boost::json::value config = boost::json::parse(R"({
    "id": "upload",
    "initial": "idle",
    "context": {"attempts": 0},
    "states": {
        "idle": {"on": {"UPLOAD": {"target": "uploading", "actions": "notify"}}},
        "uploading": {
            "always": {
                "target": "done",
                "actions": ["progress", "countAttempt", "fail"]
            }
        },
        "done": {}
    }
})");
const xstate::result<xstate::machine> upload = xstate::create_machine(config, implementations);
if (!upload.has_value()) {
    return 1;
}
const xstate::snapshot idle = xstate::get_initial_snapshot(*upload);

xstate::macrostep cursor =
    xstate::begin(*upload, idle, xstate::event{.type = "UPLOAD", .payload = {}});
while (!cursor.done()) {
    const xstate::progress made = cursor.next();
    for (const xstate::action& action : made.step.actions) {
        execute(action);
    }
    for (const xstate::action& action : made.resolved_before_failure) {
        execute(action);
    }
}
const xstate::snapshot& settled = cursor.result().snapshot;
std::cout << "status: " << name_of(settled.status) << " (" << settled.error.message() << ")\n";
std::cout << "value: " << boost::json::serialize(settled.value) << '\n';
std::cout << "context: " << boost::json::serialize(settled.context) << '\n';

It prints:

ran notify
ran progress
status: error (implementation_failed)
value: "idle"
context: {"attempts":0}

The first microstep returned notify and moved to uploading. The second failed at fail, after progress had resolved and countAttempt had counted. It reports no actions, as XState’s pure functions throw; its resolved_before_failure holds progress, which XState’s actor has run by then. The context is the one the macrostep began from, attempts at 0, although notify and progress have run: actions executed as they come are not undone, so they must tolerate a macrostep that fails after them. result().actions holds notify, from the microstep before the failure, and nothing of the failing one.

A guard of an eventless transition fails differently. The cursor evaluates it after the microstep before it, to decide whether the macrostep settles, so the next() that ran that microstep reports its transitions and its actions, with the snapshot the macrostep began from in error, settles, and leaves resolved_before_failure empty; result().actions keeps those actions too. Here GO returns a custom action, an emit and a log, and the guard of the eventless transition of checking fails. The loop prints the step the cursor reported, which an actor of XState does not show, and then what XState’s actor prints for the same machine (types_of is the helper of the cursor example, name_of that of the input example):

/**
 Runs what XState's actor runs as it resolves an action: a custom action,
 and a log, which it hands to its logger.

 Tip: XState's actor holds what an emit, the other built-in action here,
 does until the macrostep ends, which a macrostep that fails never reaches.
*/
void execute(const xstate::action& action) {
    if (!action.builtin) {
        std::cout << "ran " << action.type << '\n';
        return;
    }
    if (action.type != "xstate.log" || !action.params.is_object()) {
        return;
    }
    const boost::json::value* value = action.params.get_object().if_contains("value");
    if (value == nullptr || !value->is_string()) {
        return;
    }
    const std::string_view text = value->get_string();
    std::cout << "logged: " << text << '\n';
}

const xstate::event_maker told =
    [](const xstate::action_args&) -> xstate::result<xstate::event> {
    return xstate::event{.type = "told", .payload = {}};
};
const xstate::value_maker said =
    [](const xstate::action_args&) -> xstate::result<std::optional<boost::json::value>> {
    return std::optional<boost::json::value>("say");
};
const xstate::predicate broken = [](const xstate::action_args&) -> xstate::result<bool> {
    return xstate::failure<bool>(xstate::errc::implementation_failed);
};
xstate::implementations implementations;
implementations.actions.emplace("tell", xstate::emit_action{.event = told});
implementations.actions.emplace("say",
                                xstate::log_action{.value = said, .label = std::nullopt});
implementations.guards.emplace("broken", broken);

const boost::json::value config = boost::json::parse(R"({
    "id": "order",
    "initial": "idle",
    "states": {
        "idle": {"on": {"GO": {"target": "checking", "actions": ["note", "tell", "say"]}}},
        "checking": {"always": {"target": "done", "guard": "broken"}},
        "done": {}
    }
})");
const xstate::result<xstate::machine> order = xstate::create_machine(config, implementations);
if (!order.has_value()) {
    return 1;
}
const xstate::snapshot idle = xstate::get_initial_snapshot(*order);

xstate::macrostep cursor =
    xstate::begin(*order, idle, xstate::event{.type = "GO", .payload = {}});
while (!cursor.done()) {
    const xstate::progress made = cursor.next();
    std::cout << "step: " << types_of(made.step.actions) << (made.settled ? ", settled" : "")
              << '\n';
    for (const xstate::action& action : made.step.actions) {
        execute(action);
    }
    for (const xstate::action& action : made.resolved_before_failure) {
        execute(action);
    }
}
const xstate::snapshot& settled = cursor.result().snapshot;
std::cout << "status: " << name_of(settled.status) << " (" << settled.error.message() << ")\n";
std::cout << "value: " << boost::json::serialize(settled.value) << '\n';

It prints:

step: [note, xstate.emit, xstate.log], settled
ran note
logged: say
status: error (implementation_failed)
value: "idle"

The step holds the emit, as result().actions does, and the caller drops it, since XState’s actor never delivers told: a listener of told on that actor hears nothing.

One rule covers every macrostep that fails. XState’s actor runs a log as it resolves it, and holds what every other built-in action does until the macrostep ends, all but the part of a stop that runs at once (Executing actions as they come). A macrostep that throws never gets there: nothing it held runs, and no child the macrostep spawned starts. A caller that acts as XState’s actor therefore runs, of a macrostep that fails, the custom actions and the logs, whether it reads them from progress.step.actions, resolved_before_failure or result().actions, and drops every other built-in action but for that part of a stop: of a child it started earlier, it releases the systemIds of the child’s family and leaves the child running. Of an initial macrostep that fails it runs nothing at all: XState’s actor runs the initial actions only once it has started, and the start of an actor whose initial macrostep failed runs none. The actor layer does the same (Watching an actor).

Neither that caller nor the actor layer copies XState in one respect. XState’s actor constructs a child when it resolves the spawn, so a child that a failing macrostep spawned stays constructed and never starts, and the systemIds of its family can stay registered: a stop of that child releases them only outside an initial macrostep, which holds that part of a stop too. The caller, which drops the xstate.spawnChild, never creates that child; the actor layer creates it and then stops it, which releases those systemIds (The children of a failing macrostep). The error codes are listed under errc in Reference.

Delayed events and the simulated clock

A delayed event is one the machine asks to receive later: a state’s after, or a raise_action with a delay. The machine never reads a clock, so a step stays a function of the snapshot and the event, and time is what the caller brings (Delayed transitions). Entering a state with after returns, for each of its delays, an xstate.raise whose params hold the event, its id and the delay in milliseconds; leaving the state returns an xstate.cancel of each id. A delayed raise_action comes back the same way and is not queued. Keeping the time is the caller’s work, and simulated_clock does it: XState’s SimulatedClock with the scheduler of its actor system, as a value the caller owns.

Here a light leaves green after 1000 milliseconds and yellow after 500, unless STOP takes it to red first. The caller hands every action a step returns to the clock, moves the time, and delivers each event the clock hands back as a macrostep of its own:

const boost::json::value config = boost::json::parse(R"({
    "id": "light",
    "initial": "green",
    "states": {
        "green": {"after": {"1000": "yellow"}},
        "yellow": {"after": {"500": "red"}, "on": {"STOP": "red"}},
        "red": {}
    }
})");
const xstate::result<xstate::machine> light = xstate::create_machine(config, {});
if (!light.has_value()) {
    return 1;
}

xstate::simulated_clock clock;
auto [now, initial_actions] = xstate::initial_transition(*light);
for (const xstate::action& returned : initial_actions) {
    clock.apply(returned);
}

const auto deliver = [&](const xstate::event& happened) {
    auto [next, actions] = xstate::transition(*light, now, happened);
    for (const xstate::action& returned : actions) {
        clock.apply(returned);
    }
    now = std::move(next);
    std::cout << "  " << happened.type << " -> " << boost::json::serialize(now.value) << '\n';
};

const std::vector<std::uint64_t> times{500, 1000, 1200, 2000};
for (const std::uint64_t time : times) {
    if (!clock.set(time).has_value()) {
        return 1;
    }
    std::cout << "at " << time << ":\n";
    if (time == 1200) {
        deliver(xstate::event{.type = "STOP", .payload = {}});
    }
    while (const std::optional<xstate::event> due = clock.pop_due()) {
        deliver(*due);
    }
}

It prints:

at 500:
at 1000:
  xstate.after.1000.light.green -> "yellow"
at 1200:
  STOP -> "red"
at 2000:

Entering green returned the raise of xstate.after.1000.light.green, which the clock released at 1000. Entering yellow then returned the raise of xstate.after.500.light.yellow, due at 1500; STOP left yellow at 1200, its exit returned the cancel, and nothing fired at 2000.

XState’s pure functions leave the same work to their caller, which keeps its timers on a SimulatedClock: setTimeout holds a function that delivers the event, and moving the time calls it. A simulated_clock holds the event itself and hands it back, so the caller delivers it and applies what that delivery returns before it takes the next (The core’s clock hands over the due events).

simulated_clock has these members:

apply(action)

schedules a built-in xstate.raise that has a delay, and cancels on a built-in xstate.cancel. A delayed built-in xstate.sendTo with an id is not the machine’s to receive: the clock holds only that id, as XState’s scheduler keys every delayed event by its id, and pop_due() never returns it. A custom action changes nothing, whatever its type (builtin tells the two kinds apart, Actions), and neither does any other action.

schedule(event, delay, id)

holds an event for delay milliseconds, under an id or none. A later schedule under the same id takes the id over: the earlier event still fires, but no cancel reaches it any more, as in XState’s scheduler.

cancel(id)

forgets the event that holds id, the latest scheduled under it. Once an event scheduled under id has fired, cancel(id) cancels nothing, as XState’s scheduler forgets the id when its timer fires.

set(time), increment(milliseconds), now()

move and read the time. set refuses to move it back, with invalid_config, where XState’s set throws. Neither set nor increment releases an event, where XState’s run the timeouts that are due: pop_due() releases them.

pop_due()

takes out the earliest event that is due, by deadline and then by the order it was scheduled in, or std::nullopt when none is.

The caller delivers each due event as a macrostep of its own and applies what that macrostep returns before the next pop_due(), so a delivered event can cancel a later one, or schedule one that is due already. Time is whole milliseconds, a std::uint64_t, where XState’s clock takes fractions (Time in whole milliseconds), and equal deadlines come out in the order they were scheduled, which is what XState’s sort does under V8 (Equal deadlines). The actor layer keeps the timers of its actors itself, released by actor_system::clock_tick(now) (Delayed events and time).

Actors: xstate on xactor

The machine core computes what a machine does and returns what it does not do itself: a child to invoke or spawn, a child to stop, an event for another actor, a timer to arm, a custom action or a log for the host (the actions a step returns). A caller that steps a machine with the pure functions executes those actions itself. The actor layer, <webcpp/xstate/actors.hpp>, executes them: it runs machines as XState’s actors, so that their children run, their events reach each other, their done and error events reach their parents, their timers fire when the host brings the time, and the work only the host can do waits for the host’s answer. The split is XState’s own: its machines resolve an action, its actors execute it.

The core includes nothing of the layer and runs no actor. It knows a machine’s actors as data, the implementations’ actors and the snapshot’s children, and resolves each invoke and spawn into the xstate.spawnChild and xstate.stopChild actions it returns; the layer is the caller that executes them. The layer runs on xactor, webcpp’s deterministic actor system: one scheduler on the host’s thread, a mailbox for each actor, fuel for each execution, and timers. The core includes nothing of xactor; the layer’s headers do. This chapter uses xactor only through the layer; xactor’s page teaches it alone.

The actor model explains the ideas first, in plain words, each with one small program, and Actors takes the layer in depth. It starts with one actor: how it is created, watched and given its input. Then come the children it invokes and spawns, the events actors send each other, the system, where an actor is found by name, time, host actors, the port’s stand-in for XState’s promises, and fuel, which bounds the work of every call. The last two sections say how the layer runs and where it differs from XState.

The actor model

The actor model is a model of concurrent computation whose building block is the actor: an independent, live object that reaches others only by messages.

  • An actor has its own state, which only it changes, in response to a message it receives.

  • It talks to other actors only by sending them messages. In XState, and in xstate, the messages are events.

  • It handles one message at a time, from its mailbox, the queue of the messages it has received.

  • It shares its state only by sending events, or by publishing snapshots to whoever subscribed to them.

  • It can create actors, its children.

These rules let each actor be understood on its own. Nothing else changes its state, and what it does depends only on the messages it receives, in the order it receives them, so a program of many processes becomes a set of small ones that talk, each of which can be tested by the events it takes and sends.

A statechart describes the logic of one actor; the actor model describes how many of them live together. When XState runs a machine, the machine becomes an actor; xstate’s actor layer does the same. This section takes the model’s ideas one at a time, each with the smallest program that shows it, and the section after it, Actors, takes each in depth.

Actors

An actor runs its logic, what XState calls actor logic: here a machine. A host_actor, whose work the host does, is the other kind of logic xstate knows (Spawning). A machine actor holds the current snapshot, takes one event at a time, and moves its snapshot on with each. Nothing outside it changes its state; the host sends it events and reads, or subscribes to, its snapshots. Here the dog that wakes up and falls asleep (Events and transitions) runs as an actor, and a listener prints each snapshot it settles in:

/** Whether a call that delivers ran to its end: no error, and no actor left parked. */
bool settled(const xstate::result<xstate::run_outcome>& ran) {
    return ran.has_value() && *ran == xstate::run_outcome::settled;
}
const boost::json::value config = boost::json::parse(R"({
    "id": "dog",
    "initial": "asleep",
    "states": {
        "asleep": {"on": {"wakes up": "awake"}},
        "awake": {"on": {"falls asleep": "asleep"}}
    }
})");
const xstate::result<xstate::machine> dog = xstate::create_machine(config, {});
if (!dog.has_value()) {
    return 1;
}

xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> dog_actor = system.create_actor(*dog);
if (!dog_actor.has_value()) {
    return 1;
}
const xstate::result<void> subscribed =
    system.subscribe(*dog_actor, [](const xstate::snapshot& snapshot) {
        std::cout << "snapshot: " << boost::json::serialize(snapshot.value) << '\n';
    });
if (!subscribed.has_value()) {
    return 1;
}
if (!settled(system.start(*dog_actor))) {
    return 1;
}
for (const std::string_view type : {"wakes up", "falls asleep"}) {
    if (!settled(system.send(*dog_actor, {.type = std::string(type), .payload = {}}))) {
        return 1;
    }
}
if (!system.stop(*dog_actor).has_value()) {
    return 1;
}
const xstate::result<xactor::status> ended = system.status_of(*dog_actor);
if (!ended.has_value()) {
    return 1;
}
std::cout << "status: " << status_name(*ended) << '\n';

It prints:

snapshot: "asleep"
snapshot: "awake"
snapshot: "asleep"
status: stopped

actor_system is XState’s createActor and the actor’s system in one. create_actor makes an actor of a machine, start runs its initial macrostep, send delivers an event, and stop ends a root actor with all its children. subscribe hands a listener each snapshot the actor settles in while it is active, and the one it is done in. A failed actor’s snapshot is not handed over, as XState hands a failure to an observer’s error rather than to its next; status_of says when an actor has failed. Each call that delivers runs the system until nothing is left to do before it returns, as XState’s send processes its event at once, and says whether everything settled or an actor ran out of fuel (The actor layer). The program’s settled checks that a call returned no error and left no actor parked; the examples below use it too. An actor’s status, active, done, error or stopped, comes from status_of, where XState writes it in the snapshot.

A listener runs inside the actor’s turn. It may read the system, with snapshot_of for example, but a call from it that changes the system, a send among them, returns invalid_argument and changes nothing; XState accepts a send from a subscriber and handles it after the update in progress (A callback that calls the system back).

Messages and mailboxes

Actors talk by sending messages; to a machine actor, a message is an event. Each actor has a mailbox, where messages wait in the order they arrived, and it takes the next one only when it has finished with the one before. A machine actor has finished with an event when its macrostep has settled, so one event never interrupts another: this is XState’s run to completion. So no event ever finds an actor halfway through another: each starts from the snapshot the one before settled in.

A machine sends an event with a send_to_action, XState’s sendTo, naming its target by id: here the owner names the child it invokes, dog. The example registers the dog’s machine as an actor, a machine_actor in implementations.actors, which is XState’s setup({ actors }), invokes it from the owner, and finds it with child_of; Spawning, next, explains all three. The owner’s throw sends the dog fetch, and the dog counts the fetches in its context:

/** One more fetch, merged into the dog's context. */
xstate::result<boost::json::object> count(const xstate::action_args& args) {
    const boost::json::object* context = args.context.if_object();
    const boost::json::value* fetched =
        context == nullptr ? nullptr : context->if_contains("fetched");
    if (fetched == nullptr || !fetched->is_int64()) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"fetched", fetched->get_int64() + 1}};
}

/** The event the owner sends its dog. */
xstate::result<xstate::event> fetch(const xstate::action_args& /*args*/) {
    return xstate::event{.type = "fetch", .payload = {}};
}
xstate::implementations dog_implementations;
dog_implementations.actions.emplace("count", xstate::assign_action{.assignment = count});
const boost::json::value dog_config = boost::json::parse(R"({
    "id": "dog",
    "context": {"fetched": 0},
    "on": {"fetch": {"actions": "count"}}
})");
const xstate::result<xstate::machine> dog =
    xstate::create_machine(dog_config, dog_implementations);
if (!dog.has_value()) {
    return 1;
}

const xstate::send_to_action throw_ball{
    .target = "dog",
    .event = fetch,
    .id = std::nullopt,
    .delay = std::nullopt,
};
xstate::implementations owner_implementations;
owner_implementations.actions.emplace("throw", throw_ball);
owner_implementations.actors.emplace("dog", xstate::machine_actor{*dog});
const boost::json::value owner_config = boost::json::parse(R"({
    "id": "owner",
    "invoke": {"id": "dog", "src": "dog"},
    "on": {"play": {"actions": ["throw", "throw"]}}
})");
const xstate::result<xstate::machine> owner =
    xstate::create_machine(owner_config, owner_implementations);
if (!owner.has_value()) {
    return 1;
}

xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> owner_actor = system.create_actor(*owner);
if (!owner_actor.has_value() || !settled(system.start(*owner_actor))) {
    return 1;
}
const std::optional<xstate::actor_ref> dog_actor = system.child_of(*owner_actor, "dog");
if (!dog_actor.has_value()) {
    return 1;
}
const xstate::result<void> subscribed =
    system.subscribe(*dog_actor, [](const xstate::snapshot& snapshot) {
        std::cout << "dog: " << boost::json::serialize(snapshot.context) << '\n';
    });
if (!subscribed.has_value()) {
    return 1;
}
if (!settled(system.send(*owner_actor, {.type = "play", .payload = {}}))) {
    return 1;
}

It prints:

dog: {"fetched":1}
dog: {"fetched":2}

The owner sends two fetch events in one macrostep. They leave once that macrostep has settled, and the dog takes them one at a time, each a macrostep of its own, in the order they were sent.

xactor’s scheduler gives each actor a FIFO mailbox and keeps one FIFO queue of the actors that have messages waiting. Sending a message only puts it in its target’s mailbox; one turn at a time, on one thread, the scheduler hands the actor at the front of the queue its next message, and puts the actor back at the end of the queue while it has more. So a delivery is queued, never nested in the sender’s call, a run depends on nothing but the order in which messages were queued, and the same input always gives the same run (xactor’s guarantees 1 and 2). An actor receives what one sender sends it in the order sent. Between actors the order can differ from XState’s, which delivers an event to an idle actor at once, nested in the sender’s call: a subscriber of the owner here would hear the owner’s snapshot before the dog’s two, where XState’s hears it after them (Interleaving across actors).

Spawning

An actor can create actors, its children, to hand work to: a request the machine waits on, a process with states of its own, one helper for each item of a list. A machine does it in two ways. A state’s invoke starts a child when the state is entered and stops it when the state is left. A spawn_child_action, XState’s spawnChild, starts one that lives until a stop_child_action, XState’s stopChild, stops it, it ends, or its parent is done or stopped; a parent that fails leaves it running, as in XState. Either way the parent knows the child by an id and sends it events. A child that is done or fails tells its parent with an event: xstate.done.actor.<id>, with its output, or xstate.error.actor.<id>, with its error. An invoke takes these with its onDone and onError; a spawned child’s are taken under on. A child that is stopped sends no event.

Here the owner invokes its dog while it walks, and goes home when the dog has fetched its stick:

const boost::json::value dog_config = boost::json::parse(R"({
    "id": "dog",
    "initial": "sniffing",
    "states": {
        "sniffing": {"on": {"finds a stick": "fetched"}},
        "fetched": {"type": "final"}
    },
    "output": {"brought": "a stick"}
})");
const xstate::result<xstate::machine> dog = xstate::create_machine(dog_config, {});
if (!dog.has_value()) {
    return 1;
}

xstate::implementations implementations;
implementations.actors.emplace("dog", xstate::machine_actor{*dog});
const boost::json::value owner_config = boost::json::parse(R"({
    "id": "owner",
    "initial": "walking",
    "states": {
        "walking": {"invoke": {"id": "dog", "src": "dog", "onDone": "home"}},
        "home": {"type": "final"}
    }
})");
const xstate::result<xstate::machine> owner =
    xstate::create_machine(owner_config, implementations);
if (!owner.has_value()) {
    return 1;
}

xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> owner_actor = system.create_actor(*owner);
if (!owner_actor.has_value() || !settled(system.start(*owner_actor))) {
    return 1;
}
const std::optional<xstate::actor_ref> dog_actor = system.child_of(*owner_actor, "dog");
if (!dog_actor.has_value()) {
    return 1;
}
print("owner", system.snapshot_of(*owner_actor));
print("dog", system.snapshot_of(*dog_actor));

if (!settled(system.send(*dog_actor, {.type = "finds a stick", .payload = {}}))) {
    return 1;
}
print("owner", system.snapshot_of(*owner_actor));
print("dog", system.snapshot_of(*dog_actor));

print, a helper of the program, writes who an actor is, its state value, its status, and its output when it has one. It prints:

owner: "walking", active
dog: "sniffing", active
owner: "home", done
dog: "fetched", done, output {"brought":"a stick"}

An invoke names its child by src, which the implementations register, as XState’s setup({ actors }) does: a machine_actor for a machine, or a host_actor for work done outside the machines, which XState writes as a promise. child_of finds a child by its parent and its id. When the dog reaches its final state it is done, with its output; the owner receives xstate.done.actor.dog, and its onDone takes it home. XState also takes inline logic as a src; JSON cannot hold it, so xstate takes a name only (An invoke’s src).

The same owner can spawn the same dog instead: an entry action spawns it, and the owner takes its done event under on, since onDone belongs to an invoke. The dog’s machine is the one above:

const xstate::spawn_child_action adopt{
    .src = "dog",
    .id = std::string("dog"),
    .system_id = std::nullopt,
    .input = std::nullopt,
};
xstate::implementations implementations;
implementations.actors.emplace("dog", xstate::machine_actor{*dog});
implementations.actions.emplace("adopt", adopt);
const boost::json::value owner_config = boost::json::parse(R"({
    "id": "owner",
    "initial": "walking",
    "states": {
        "walking": {
            "entry": "adopt",
            "on": {"xstate.done.actor.dog": "home"}
        },
        "home": {"type": "final"}
    }
})");
const xstate::result<xstate::machine> owner =
    xstate::create_machine(owner_config, implementations);
if (!owner.has_value()) {
    return 1;
}

xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> owner_actor = system.create_actor(*owner);
if (!owner_actor.has_value() || !settled(system.start(*owner_actor))) {
    return 1;
}
const std::optional<xstate::actor_ref> dog_actor = system.child_of(*owner_actor, "dog");
if (!dog_actor.has_value()) {
    return 1;
}
print("owner", system.snapshot_of(*owner_actor));

if (!settled(system.send(*dog_actor, {.type = "finds a stick", .payload = {}}))) {
    return 1;
}
print("owner", system.snapshot_of(*owner_actor));
print("dog", system.snapshot_of(*dog_actor));

It prints:

owner: "walking", active
owner: "home", done
dog: "fetched", done, output {"brought":"a stick"}

adopt is a built-in action registered by name, as throw is in Messages and mailboxes; it names the child’s src and its id. Invoking actors, Spawning actors and Host actors take each in depth.

The system

A system is the set of actors that live together: a root actor, with its children and theirs. An actor can be registered in the system under a name, its systemId, and any actor of the system can then reach it by that name alone, without holding a reference to it or knowing where it sits in the family. A name is for actors that need each other but are not parent and child: two siblings, or a service that actors in every branch of the family call.

A machine names a registered actor with the target #system:<systemId>. XState writes that target as a function of the system, ({ system }) => system.get('vet'), which a JSON config cannot hold (Targets that name actors). The host finds a registered actor with get, XState’s system.get. Here the owner invokes a vet and a dog walker, and the walker calls the vet when the dog limps:

/** The symptom event, its symptom taken from the action's params. */
xstate::result<xstate::event> symptom_event(const xstate::action_args& args) {
    const boost::json::object* params = args.params.if_object();
    const boost::json::value* symptom =
        params == nullptr ? nullptr : params->if_contains("symptom");
    if (symptom == nullptr) {
        return xstate::failure<xstate::event>(xstate::errc::implementation_failed);
    }
    return xstate::event{.type = "symptom", .payload = {{"symptom", *symptom}}};
}

/** Keeps the symptom of the event being handled. */
xstate::result<boost::json::object> note(const xstate::action_args& args) {
    const boost::json::value* symptom = args.event.payload.if_contains("symptom");
    if (symptom == nullptr) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"last", *symptom}};
}
xstate::implementations vet_implementations;
vet_implementations.actions.emplace("note", xstate::assign_action{.assignment = note});
const boost::json::value vet_config = boost::json::parse(R"({
    "id": "vet",
    "context": {"last": null},
    "on": {"symptom": {"actions": "note"}}
})");
const xstate::result<xstate::machine> vet =
    xstate::create_machine(vet_config, vet_implementations);
if (!vet.has_value()) {
    return 1;
}

const xstate::send_to_action call_vet{
    .target = "#system:vet",
    .event = symptom_event,
    .id = std::nullopt,
    .delay = std::nullopt,
};
xstate::implementations walker_implementations;
walker_implementations.actions.emplace("callVet", call_vet);
const boost::json::value walker_config = boost::json::parse(R"({
    "id": "walker",
    "on": {
        "sees a limp": {"actions": {"type": "callVet", "params": {"symptom": "limps"}}}
    }
})");
const xstate::result<xstate::machine> walker =
    xstate::create_machine(walker_config, walker_implementations);
if (!walker.has_value()) {
    return 1;
}

xstate::implementations owner_implementations;
owner_implementations.actors.emplace("vet", xstate::machine_actor{*vet});
owner_implementations.actors.emplace("walker", xstate::machine_actor{*walker});
const boost::json::value owner_config = boost::json::parse(R"({
    "id": "owner",
    "invoke": [
        {"id": "vet", "src": "vet", "systemId": "vet"},
        {"id": "walker", "src": "walker"}
    ]
})");
const xstate::result<xstate::machine> owner =
    xstate::create_machine(owner_config, owner_implementations);
if (!owner.has_value()) {
    return 1;
}

xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> owner_actor = system.create_actor(*owner);
if (!owner_actor.has_value() || !settled(system.start(*owner_actor))) {
    return 1;
}
const std::optional<xstate::actor_ref> walker_actor = system.child_of(*owner_actor, "walker");
if (!walker_actor.has_value() ||
    !settled(system.send(*walker_actor, {.type = "sees a limp", .payload = {}}))) {
    return 1;
}
const std::optional<xstate::actor_ref> vet_actor = system.get("vet");
if (!vet_actor.has_value()) {
    return 1;
}
const xstate::snapshot* heard = system.snapshot_of(*vet_actor);
if (heard == nullptr) {
    return 1;
}
std::cout << "vet: " << boost::json::serialize(heard->context) << '\n';

It prints:

vet: {"last":"limps"}

The walker and the vet are siblings: neither holds the other, and the walker reaches the vet by its systemId alone. A systemId names one actor at a time: a second claim fails the actor that makes it, with system_id_taken, and the host’s create_actor refuses a name already held. The roots one actor_system creates share one system, where XState gives each root its own, because an actor_system owns one xactor scheduler with its registry and xstate keeps no global state; a program that wants XState’s arrangement gives each root an actor_system of its own (One system per actor_system). Its rules follow in the system’s own section.

Actors

When a machine runs as an actor, the actor keeps the machine’s snapshot, takes its events one at a time and executes what each macrostep returns. It receives events, sends events to other actors and changes its behaviour by the events it receives. It can invoke and spawn other actors, its children, and it hears from them when they are done or fail.

An actor is what a machine needs once it does not live alone. The pure functions suit one machine whose caller keeps the snapshot and executes its actions (Pure transitions and the macrostep cursor). A machine that invokes another, sends it events, waits on a timer or waits on work the host does needs something to run all of that, in order, between the host’s calls. An actor does it, and the host only creates actors, sends them events, brings the time and reads what the actors publish.

Actor logic

An actor runs its logic, what XState calls actor logic. xstate knows two kinds:

Logic Receives events Sends events Has children Input Output

A machine, machine_actor: XState’s createMachine

yes

yes

yes

yes

yes

A host actor, host_actor: in place of XState’s fromPromise

no

only its done or error event

no

yes

yes

The host creates a machine actor with create_actor, and a machine invokes or spawns one where implementations.actors names a machine_actor. A host actor is always a child, which a machine invokes or spawns where implementations.actors names a host_actor, and whose work the host does (Host actors). XState’s callback, observable and transition actors have no counterpart yet (What is not ported).

Every actor lives in an actor_system: one xactor scheduler on the host’s thread, which the host drives by calling the system. The scheduler holds a first-in first-out mailbox for each actor and a first-in first-out queue of the actors that have work, and each turn hands one message to one actor (How the layer runs). So the actor model holds as XState applies it: an actor changes only its own state, talks only by events, takes one event at a time from its mailbox, its whole macrostep, and creates children.

Creating an actor

An actor_system is XState’s createActor and the system it creates, in one object. create_actor creates an actor from a machine, not yet started; start runs its initial macrostep; send delivers an event; stop stops it with its children. XState makes a system for each root actor, implicitly; xstate makes it explicit, because the system owns what a host must decide: the fuel of each execution its calls open, the time, and the systemIds its actors share. The machine is the toggle of Stately’s page on state machine actors, and a listener prints each snapshot:

const boost::json::value config = boost::json::parse(R"({
    "initial": "inactive",
    "states": {
        "inactive": {"on": {"toggle": {"target": "active"}}},
        "active": {"on": {"toggle": {"target": "inactive"}}}
    }
})");
const xstate::result<xstate::machine> toggle_machine = xstate::create_machine(config, {});
if (!toggle_machine.has_value()) {
    return 1;
}

xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> toggle_actor = system.create_actor(*toggle_machine);
if (!toggle_actor.has_value()) {
    return 1;
}
const xstate::result<void> subscribed =
    system.subscribe(*toggle_actor, [](const xstate::snapshot& snapshot) {
        std::cout << boost::json::serialize(snapshot.value) << '\n';
    });
if (!subscribed.has_value()) {
    return 1;
}

if (!system.start(*toggle_actor).has_value()) {
    return 1;
}
// logs "inactive"
const xstate::event toggle{.type = "toggle", .payload = {}};
if (!system.send(*toggle_actor, toggle).has_value()) {
    return 1;
}
// logs "active"
if (!system.send(*toggle_actor, toggle).has_value()) {
    return 1;
}
// logs "inactive"

if (!system.stop(*toggle_actor).has_value()) {
    return 1;
}
const xstate::result<xactor::status> status = system.status_of(*toggle_actor);
if (!status.has_value()) {
    return 1;
}
std::cout << "status: " << name_of(*status) << '\n';

It prints:

"inactive"
"active"
"inactive"
status: stopped

create_actor returns result<actor_ref>: the actor’s address on xactor, or system_id_taken (Options). Every operation names the actor by that address, through the system. subscribe hears the snapshots the actor settles in while it is active, and the one it is done in, as XState’s does: the initial one on start, then one for each event, whether it took a transition or not (Watching an actor).

The constructor takes budgets, xactor’s limits. Its one member, fuel, bounds the work of one execution, a million units by default. A call that delivers opens executions of its own, each with the whole budget: one for each input it applies and, for resume(), one for each parked actor. So one call can spend more than one budget (Fuel, parking and resume). The examples give 10'000, far more than they use, and check only that a call returned no error; a host with a tight budget also checks whether it returned settled or out_of_fuel.

Each call that delivers something, start and send here, and clock_tick, resolve, reject and resume in the later sections, runs the system until no message is left before it returns, as XState’s send processes its event at once. It returns result<run_outcome>: settled, or out_of_fuel when an actor is left parked for want of fuel (Fuel, parking and resume).

A call the system cannot take fails with xactor’s errc::invalid_argument, of the category webcpp.xactor, and changes nothing: a start, a send, a stop, a status_of, a subscribe or an on_emitted whose actor_ref names no actor of the system, a stop of a child, an answer to a request no longer pending (Host actors), and any call that changes the system made from inside a callback (Watching an actor). The other readers refuse nothing: snapshot_of, child_of, parent_of, id_of and get answer nullptr, none or an empty view for what they do not find. xstate’s own codes, such as the system_id_taken of create_actor, are xstate::errc, of the category webcpp.xstate, and none of them is invalid_argument.

The rules of an actor’s life:

  • An event sent before start waits, and runs after the initial macrostep, as XState’s mailbox holds it until the actor starts. A second start does nothing.

  • stop stops a root actor and its whole family, each after its descendants, and returns them. For a child it is xactor’s invalid_argument and changes nothing: a child is its parent’s to stop, as XState’s actor.stop() refuses one.

  • A stopped actor drops what is sent to it.

Snapshots and status

snapshot_of(actor) is a machine actor’s last snapshot (The snapshot), as XState’s getSnapshot().

  • It is published once the macrostep has settled and its actions are resolved, before the effects they deferred (sends, emits, starts, stops and timers) run. Subscribers hear it after those effects. An actor parked before that point (Fuel, parking and resume) still shows the snapshot from before; one parked while running the effects already shows the new one, which its subscribers have not heard yet.

  • It is nullptr before the actor is constructed, and for a host actor. xstate constructs a root on start, where XState’s createActor constructs it at once, so XState’s getSnapshot() has the initial snapshot before start() (The root is constructed on start).

status_of(actor) is xactor’s status: active, done, error or stopped. The snapshot’s own status has only the first three; XState’s has a fourth, 'stopped'. A stopped actor keeps its last snapshot, whose status stays what it was, and status_of says stopped where XState’s snapshot says 'stopped'.

Options

actor_options holds the options of XState’s createActor that concern one actor: input, id and system_id. The others belong to the system: logger is on_log (Watching an actor), inspect is inspect (Inspection), and clock is clock_tick (Delayed events and time). snapshot, which restores a persisted actor, has no counterpart yet. Here one actor is named and registered, and another is left unnamed:

xstate::actor_system system({.fuel = 10'000});
const xstate::actor_options options{
    .input = nullptr,
    .id = "feedback",
    .system_id = "root-id",
};
const xstate::result<xstate::actor_ref> named = system.create_actor(*machine, options);
if (!named.has_value()) {
    return 1;
}
const xstate::result<xstate::actor_ref> unnamed = system.create_actor(*machine);
if (!unnamed.has_value()) {
    return 1;
}
std::cout << system.id_of(*named) << '\n';    // feedback
std::cout << system.id_of(*unnamed) << '\n';  // x:1

if (!system.start(*named).has_value()) {
    return 1;
}
const std::optional<xstate::actor_ref> found = system.get("root-id");
std::cout << std::boolalpha << (found == *named) << '\n';  // true

It prints:

feedback
x:1
true
  • id is the actor’s id, which id_of(actor) returns. Without one, the id is the actor’s session id, x:<n>, n the number of actors the system created or began to create before it. XState counts the actors of the whole process, so the two agree while the process runs one actor_system with one root (One system per actor_system). The example’s two roots agree with XState only because the first has no children: XState numbers a root’s initial children when it creates the root, xstate when it starts it (The root is constructed on start).

  • system_id registers the actor under that name at once: get(system_id) finds it, and a #system: target names it (The system). A name another running actor holds is refused with system_id_taken. The empty name registers nothing, as XState registers only a truthy one.

  • input is the machine’s input (Machine actors).

Watching an actor

The host learns what its actors do through callbacks it registers on the system.

  • subscribe(actor, f) hands f each snapshot the actor settles in while it is active, and the one it is done in: XState’s actor.subscribe. A failed actor’s snapshot is not handed over; XState hands the error to an observer’s error callback, which xstate does not have, and status_of tells it.

  • on_emitted(actor, f) hands f each event the actor emits, in order: XState’s actor.on('*', f) (Emitting events). The listener reads the event’s type.

  • on_action(f) hands f every custom action of every actor, with the actor that returned it.

  • on_log(f) hands f every log of every actor (Log), with the actor, the value, nullptr for JavaScript’s undefined, and the label: XState’s logger option. The label is empty when the log has none, where XState calls the logger with the value alone.

A custom action is one the implementations do not hold (Custom actions). XState runs it as a function of the machine; xstate hands it to the host as its type and params, because JSON cannot hold a function and the library runs no effect of its own: the host decides what greet does. Here a machine greets on entry, and answers ping by emitting pong and logging the event:

xstate::result<xstate::event> pong(const xstate::action_args& /*args*/) {
    return xstate::event{.type = "pong", .payload = {}};
}

xstate::result<std::optional<boost::json::value>> the_event(const xstate::action_args& args) {
    return std::optional<boost::json::value>(xstate::to_json(args.event));
}
xstate::implementations implementations;
implementations.actions.emplace("answer", xstate::emit_action{.event = pong});
implementations.actions.emplace("note",
                                xstate::log_action{.value = the_event, .label = "received"});
// greet has no implementation: it is a custom action, the host's to run.
const boost::json::value config = boost::json::parse(R"({
    "entry": {"type": "greet", "params": {"name": "Ada"}},
    "on": {"ping": {"actions": ["answer", "note"]}}
})");
const xstate::result<xstate::machine> machine = xstate::create_machine(config, implementations);
if (!machine.has_value()) {
    return 1;
}

xstate::actor_system system({.fuel = 10'000});
const xstate::result<void> acting =
    system.on_action([](xstate::actor_ref, const xstate::action& custom) {
        std::cout << "action " << custom.type << ' ' << boost::json::serialize(custom.params)
                  << '\n';
    });
if (!acting.has_value()) {
    return 1;
}
const xstate::result<void> logging = system.on_log(
    [](xstate::actor_ref, const boost::json::value* value, std::string_view label) {
        if (value != nullptr) {
            std::cout << "log " << label << ' ' << boost::json::serialize(*value) << '\n';
        }
    });
if (!logging.has_value()) {
    return 1;
}
const xstate::result<xstate::actor_ref> actor = system.create_actor(*machine);
if (!actor.has_value()) {
    return 1;
}
const xstate::result<void> listening =
    system.on_emitted(*actor, [](const xstate::event& emitted) {
        std::cout << "emitted " << boost::json::serialize(xstate::to_json(emitted)) << '\n';
    });
if (!listening.has_value()) {
    return 1;
}

if (!system.start(*actor).has_value()) {
    return 1;
}
// logs: action greet {"name":"Ada"}
if (!system.send(*actor, {.type = "ping", .payload = {}}).has_value()) {
    return 1;
}
// logs: log received {"type":"ping"}
// logs: emitted {"type":"pong"}

It prints:

action greet {"name":"Ada"}
log received {"type":"ping"}
emitted {"type":"pong"}

The order is XState’s. Custom actions and logs are handed over as each microstep’s actions are resolved, those of the initial macrostep when the actor starts; emits, like sends, wait until the macrostep has settled, as XState defers them. So note logs before answer emits, though the machine lists answer first. A macrostep that fails has handed over the custom actions and logs it resolved before the failure, as XState’s running actor has run them by then; an initial macrostep that fails hands over none, as XState’s start() runs no initial action of an actor whose construction failed.

subscribe and on_emitted add a listener of one actor, and an empty one is none; such a listener is kept as long as the system lives, with no unsubscribe. Both refuse an address that names no actor with invalid_argument, as send does. on_action, on_log and inspect hold one callback for the whole system: each call replaces the previous one, so an empty callback removes it. Each returns a result<void>.

A callback, a subscribe or on_emitted listener, the on_action or on_log callback or the inspector, runs inside an actor’s turn, while the system runs. It may call the const members, which read the system: snapshot_of, status_of, get, child_of, parent_of, id_of and host_requests. Every other member changes the system: create_actor, start, send, clock_tick, resolve, reject, resume, run_until_idle, stop, and the registrations subscribe, on_emitted, on_action, on_log and inspect. A call to one of them from a callback returns xactor’s invalid_argument and changes nothing. A host that must act on what it heard records it in the callback and acts once the call that ran the system has returned. XState accepts a send from a subscriber and handles it after the update in progress (A callback that calls the system back).

Inspection

inspect(inspector) is XState’s inspect option: it hears every actor of the system, and replaces the inspector given before. An inspector holds three callbacks, each of which may be left empty:

XState xstate

@xstate.actor, when an actor is created

started, when an actor starts

@xstate.snapshot, each snapshot with the event that caused it

settled, each macrostep a machine actor settles, with its machine, the event that began the macrostep and the snapshot

@xstate.action of the type xstate.emit, whose params hold the event, reported when the action runs; the actor’s actor.on listeners hear the event once the macrostep has settled

emitted, each event an actor emits, just before its on_emitted listeners hear it

@xstate.action of every other type: each custom action, and each built-in action but assign and enqueueActions: a send, a raise, a log, a spawn, a stop or a cancel

none in the inspector: on_action hears the custom actions and on_log the logs, and no callback hears the other built-in actions

@xstate.event and @xstate.microstep

none: the macrostep cursor gives each microstep (The macrostep cursor)

A macrostep that an unhandled error event fails is heard, with the status error and the error actor_failed, as XState inspects the snapshot that event leaves. Any other that fails while it is computed or while its actions resolve is not heard. Nor does XState inspect one where it throws, as for an implementation that fails, a systemId taken when a spawn resolves or a target that names no actor when a send resolves; where it does not fail, on a context that is not an object (A context that is not an object), its inspector hears the macrostep as any other. A macrostep whose deferred effect fails, such as a send to a child gone when it is bound (A sendTo whose child is gone), is heard with the snapshot it reached, as XState’s update inspects the snapshot it was given; snapshot_of has the status error.

Here an inspector watches the toggle of the first example, whose active state now emits activated on entry:

xstate::actor_system system({.fuel = 10'000});
const xstate::result<void> inspecting = system.inspect(xstate::inspector{
    .started = [](xstate::actor_ref) { std::cout << "started\n"; },
    .settled =
        [](xstate::actor_ref, const xstate::machine&, const xstate::event& cause,
           const xstate::snapshot& settled) {
            std::cout << "settled " << cause.type << ' '
                      << boost::json::serialize(settled.value) << '\n';
        },
    .emitted =
        [](xstate::actor_ref, const xstate::event& emitted) {
            std::cout << "emitted " << emitted.type << '\n';
        },
});
if (!inspecting.has_value()) {
    return 1;
}
const xstate::result<xstate::actor_ref> actor = system.create_actor(*machine);
if (!actor.has_value()) {
    return 1;
}
if (!system.start(*actor).has_value()) {
    return 1;
}
if (!system.send(*actor, {.type = "toggle", .payload = {}}).has_value()) {
    return 1;
}

It prints:

started
settled xstate.init "inactive"
emitted activated
settled toggle "active"

The inspector hears the actor start, then its initial macrostep settle for xstate.init. The toggle enters active, whose entry emits activated: the inspector hears the emit while the actor runs the effects of that macrostep, before the snapshot of toggle, which it hears once those effects have run.

Machine actors

A machine actor runs a machine: it receives events, sends events, has children, starts from an input and ends with an output. create_actor creates one from a machine; a machine child is a machine_actor of the implementations (Invoking actors).

Its input is actor_options::input. The machine reads it in implementations.context, XState’s context: ({ input }) => ..., which returns result<boost::json::value>. A failure it returns fails the initial macrostep with its code, and a value that is not an object fails it with implementation_failed, where XState merges whatever its function returns into an empty object (A context that is not an object). The event xstate.init carries the input too. When the machine reaches its top-level final state, the actor is done: status_of says done, its last snapshot holds the output, and it takes no more events. The output here is the context, which implementations.outputs computes under the root’s id, XState’s output: ({ context }) => context. A machine’s input and its output have sections of their own. The example is the feedback machine of Stately’s page on state machine actors, given a final state and an output.

xstate::implementations implementations;
implementations.context =
    [](const boost::json::value& input) -> xstate::result<boost::json::value> {
    const boost::json::object* given = input.if_object();
    const boost::json::value* rating =
        given == nullptr ? nullptr : given->if_contains("defaultRating");
    if (rating == nullptr) {
        return xstate::failure<boost::json::value>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"rating", *rating}};
};
// By the root's id: the machine's output.
implementations.outputs.emplace(
    "feedback",
    [](const xstate::action_args& args) -> xstate::result<std::optional<boost::json::value>> {
        return std::optional<boost::json::value>(args.context);
    });
const boost::json::value config = boost::json::parse(R"({
    "id": "feedback",
    "initial": "question",
    "states": {
        "question": {"on": {"submit": "thanks"}},
        "thanks": {"type": "final"}
    }
})");
const xstate::result<xstate::machine> feedback_machine =
    xstate::create_machine(config, implementations);
if (!feedback_machine.has_value()) {
    return 1;
}

xstate::actor_system system({.fuel = 10'000});
const xstate::actor_options options{
    .input = boost::json::object{{"defaultRating", 3}},
    .id = std::nullopt,
    .system_id = std::nullopt,
};
const xstate::result<xstate::actor_ref> feedback_actor =
    system.create_actor(*feedback_machine, options);
if (!feedback_actor.has_value()) {
    return 1;
}
const xstate::result<void> subscribed =
    system.subscribe(*feedback_actor, [](const xstate::snapshot& snapshot) {
        std::cout << boost::json::serialize(snapshot.context) << '\n';
    });
if (!subscribed.has_value()) {
    return 1;
}
if (!system.start(*feedback_actor).has_value()) {
    return 1;
}
// logs {"rating":3}
if (!system.send(*feedback_actor, {.type = "submit", .payload = {}}).has_value()) {
    return 1;
}
// logs {"rating":3}, the snapshot it is done in

const xstate::result<xactor::status> status = system.status_of(*feedback_actor);
if (!status.has_value()) {
    return 1;
}
const xstate::snapshot* last = system.snapshot_of(*feedback_actor);
if (last == nullptr) {
    return 1;
}
if (!last->output.has_value()) {
    return 1;
}
std::cout << "status: " << name_of(*status) << '\n'
          << "output: " << boost::json::serialize(*last->output) << '\n';

It prints:

{"rating":3}
{"rating":3}
status: done
output: {"rating":3}

The listener hears the initial snapshot, its context computed from the input, and the snapshot the actor is done in, after submit; that last snapshot holds the output.

Invoking actors

A state can invoke an actor: a child that starts when the state is entered and stops when the state is exited. Invoking suits work the machine orchestrates without knowing its details: a request to a server, a process with states of its own. An invoke on the root lives as long as the machine. A state’s invoke is one object or a list of them, each a child of its own.

An invoke’s members:

src

The name of an entry of implementations.actors: a machine_actor or a host_actor. XState also takes inline logic, which JSON cannot hold; a name is XState’s setup({ actors }), the logic registered there. A name the implementations do not hold is refused when the machine is created, with unknown_actor, where XState fails later.

id

How the parent knows the child: <index>.<state id> when none is given, as in XState.

input

The child’s input: a JSON value (Input to an invoked actor), or what implementations.inputs computes for the invoke’s id, in place of XState’s input function (An input computed from the context and the event).

onDone

The transition taken on xstate.done.actor.<id>, when the child is done.

onError

The transition taken on xstate.error.actor.<id>, when the child fails.

systemId

A name every actor of the system can find the child by (The system).

A machine can declare onSnapshot too, but the layer sends no xstate.snapshot.<id> event yet, so it is never taken.

The host reaches a child through its parent: child_of(parent, id) is XState’s snapshot.children[id], and the parent’s snapshot lists its children as (id, src) pairs. parent_of(child) goes the other way. Here a parent invokes a worker and takes its output when it is done.

const boost::json::value worker_config = boost::json::parse(R"({
    "initial": "working",
    "states": {
        "working": {"on": {"finish": "done"}},
        "done": {"type": "final"}
    },
    "output": {"result": 42}
})");
const xstate::result<xstate::machine> worker = xstate::create_machine(worker_config, {});
if (!worker.has_value()) {
    return 1;
}

xstate::implementations implementations;
implementations.actors.emplace("worker", xstate::machine_actor{*worker});
implementations.actions.emplace("note",
                                xstate::log_action{.value = the_event, .label = "done"});
const boost::json::value parent_config = boost::json::parse(R"({
    "initial": "waiting",
    "states": {
        "waiting": {
            "invoke": {
                "src": "worker",
                "id": "worker",
                "onDone": {"target": "finished", "actions": "note"}
            }
        },
        "finished": {}
    }
})");
const xstate::result<xstate::machine> parent_machine =
    xstate::create_machine(parent_config, implementations);
if (!parent_machine.has_value()) {
    return 1;
}
xstate::actor_system system({.fuel = 10'000});
const xstate::result<void> logging = system.on_log(
    [](xstate::actor_ref, const boost::json::value* value, std::string_view label) {
        if (value != nullptr) {
            std::cout << label << ' ' << boost::json::serialize(*value) << '\n';
        }
    });
if (!logging.has_value()) {
    return 1;
}
const xstate::result<xstate::actor_ref> parent = system.create_actor(*parent_machine);
if (!parent.has_value()) {
    return 1;
}
if (!system.start(*parent).has_value()) {
    return 1;
}
print(system, *parent);  // "waiting" ["worker"]

const std::optional<xstate::actor_ref> child = system.child_of(*parent, "worker");
if (!child.has_value()) {
    return 1;
}
if (!system.send(*child, {.type = "finish", .payload = {}}).has_value()) {
    return 1;
}
// logs the done event onDone took
print(system, *parent);  // "finished" []

const xstate::result<xactor::status> status = system.status_of(*child);
if (!status.has_value()) {
    return 1;
}
std::cout << "worker: " << name_of(*status) << '\n';

print, a helper of the program, writes the parent’s state value and the ids of its children. It prints:

"waiting" ["worker"]
done {"type":"xstate.done.actor.worker","output":{"result":42},"actorId":"worker"}
"finished" []
worker: done

Entering waiting starts the worker, which the parent lists as its child. When the worker reaches its final state, the parent receives xstate.done.actor.worker with the worker’s output, and onDone logs that event and takes the parent to finished. Leaving waiting removes the worker from the parent’s children; the worker itself is done.

A child’s life follows its parent’s macrosteps, as in XState:

  • A machine child is constructed when its parent resolves the action that spawns it: its initial macrostep runs and its own children are constructed, their systemIds registered, before the parent resolves its next action, so a #system: send later in the same macrostep reaches a grandchild. It starts once the parent’s macrostep has settled, and the custom actions and logs of its initial macrostep are handed over then, none when that macrostep failed (Watching an actor).

  • A child spawned and stopped in the same macrostep never starts: XState constructs it and never starts it, so a state entered and left in one macrostep runs none of its invokes.

  • A transition that re-enters the state (reenter: true) stops its invoked children and starts new ones.

Done and error events

A child that ends tells its parent:

  • xstate.done.actor.<id>, with output and actorId, when it is done; the output is left out when it has none (Actor done events).

  • xstate.error.actor.<id>, with error and actorId, when it fails. A host actor’s error is what the host rejected it with. A machine child’s is the error of the child whose failure failed it in turn, or else the name of its own error code, such as "implementation_failed", where XState’s is the value its implementation threw.

An error event that no transition takes fails the parent too, and the error climbs unchanged: the parent’s snapshot has the status error, the error actor_failed and, as error_value, the event’s error, and the parent reports to its own parent. Here a payment fails, the checkout that invoked it has no onError, and the shop above it handles the checkout’s error. The payment is a host actor, xstate’s stand-in for XState’s promise actor, which the host answers (Host actors): host_requests() lists it as a pending request, and reject fails it with the error given, as rejecting its promise fails XState’s promise actor.

xstate::implementations checkout_implementations;
checkout_implementations.actors.emplace("charge", xstate::host_actor{});
// No onError: the payment's error fails the checkout.
const boost::json::value checkout_config = boost::json::parse(R"({
    "initial": "paying",
    "states": {
        "paying": {"invoke": {"src": "charge", "id": "payment"}}
    }
})");
const xstate::result<xstate::machine> checkout =
    xstate::create_machine(checkout_config, checkout_implementations);
if (!checkout.has_value()) {
    return 1;
}

xstate::implementations shop_implementations;
shop_implementations.actors.emplace("checkout", xstate::machine_actor{*checkout});
shop_implementations.actions.emplace("note",
                                     xstate::log_action{.value = the_event, .label = "shop"});
const boost::json::value shop_config = boost::json::parse(R"({
    "initial": "open",
    "states": {
        "open": {
            "invoke": {
                "src": "checkout",
                "id": "checkout",
                "onError": {"target": "failed", "actions": "note"}
            }
        },
        "failed": {}
    }
})");
const xstate::result<xstate::machine> shop_machine =
    xstate::create_machine(shop_config, shop_implementations);
if (!shop_machine.has_value()) {
    return 1;
}
xstate::actor_system system({.fuel = 10'000});
const xstate::result<void> logging = system.on_log(
    [](xstate::actor_ref, const boost::json::value* value, std::string_view label) {
        if (value != nullptr) {
            std::cout << label << ' ' << boost::json::serialize(*value) << '\n';
        }
    });
if (!logging.has_value()) {
    return 1;
}
const xstate::result<xstate::actor_ref> shop = system.create_actor(*shop_machine);
if (!shop.has_value()) {
    return 1;
}
if (!system.start(*shop).has_value()) {
    return 1;
}
const std::optional<xstate::actor_ref> checkout_actor = system.child_of(*shop, "checkout");
if (!checkout_actor.has_value()) {
    return 1;
}
const std::vector<xstate::host_request> requests = system.host_requests();
if (requests.empty()) {
    return 1;
}
const boost::json::object declined{{"code", "declined"}};
if (!system.reject(requests.front(), declined).has_value()) {
    return 1;
}
// logs the error event onError took

const xstate::result<xactor::status> status = system.status_of(*checkout_actor);
if (!status.has_value()) {
    return 1;
}
const xstate::snapshot* failed = system.snapshot_of(*checkout_actor);
if (failed == nullptr) {
    return 1;
}
if (!failed->error_value.has_value()) {
    return 1;
}
std::cout << "checkout: " << name_of(*status) << ' '
          << boost::json::serialize(*failed->error_value) << '\n';
std::cout << "error code: " << failed->error.message() << '\n';

It prints:

shop {"type":"xstate.error.actor.checkout","error":{"code":"declined"},"actorId":"checkout"}
checkout: error {"code":"declined"}
error code: actor_failed

The checkout’s snapshot holds the code actor_failed in error, since every failure in xstate is an error_code, and the payment’s error in error_value, which the shop’s event carries unchanged. XState’s snapshot holds the value itself in error, and has no code to print (Failures are values). A failed actor’s children keep running, as in XState; a done actor’s were stopped by its last macrostep.

Spawning actors

Invoking ties a child to a state. Some children must outlive a state, or come in a number the machine learns as it runs, one per item of a list. Those are spawned: spawn_child_action, XState’s spawnChild, creates a child that lives until a stop_child_action, XState’s stopChild, stops it, or until its parent is done or stopped; a failed parent’s children keep running, as in XState (Done and error events).

spawn_child_action holds XState’s options: src, the actor’s name in implementations.actors; id, a string, or an id_maker that computes it when the action resolves; system_id; and input, a value_maker (Input to a spawned actor). An id left out is the empty string, and the child is keyed under "", where XState keys a child spawned without an id "undefined" (A spawned child without an id). XState’s other way to spawn, spawn inside assign, keeps an actor reference in the context, which JSON cannot hold; it has no counterpart. The example is the one of Stately’s page on spawning, with its children registered by name. spawning makes each spawn action:

/** XState's spawnChild('childMachine', { id }). */
xstate::spawn_child_action spawning(std::string_view id) {
    return xstate::spawn_child_action{
        .src = "childMachine",
        .id = std::string(id),
        .system_id = std::nullopt,
        .input = std::nullopt,
    };
}
const boost::json::value child_config = boost::json::parse(R"({"id": "child"})");
const xstate::result<xstate::machine> child_machine = xstate::create_machine(child_config, {});
if (!child_machine.has_value()) {
    return 1;
}

xstate::implementations implementations;
implementations.actors.emplace("childMachine", xstate::machine_actor{*child_machine});
implementations.actions.emplace("spawnChild1", spawning("child-1"));
implementations.actions.emplace("spawnChild2", spawning("child-2"));
implementations.actions.emplace("spawnChild3", spawning("child-3"));
implementations.actions.emplace("stopChild2", xstate::stop_child_action{.id = "child-2"});
const boost::json::value config = boost::json::parse(R"({
    "entry": ["spawnChild1", "spawnChild2", "spawnChild3"],
    "on": {"STOP": {"actions": "stopChild2"}}
})");
const xstate::result<xstate::machine> parent_machine =
    xstate::create_machine(config, implementations);
if (!parent_machine.has_value()) {
    return 1;
}
xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> parent = system.create_actor(*parent_machine);
if (!parent.has_value()) {
    return 1;
}
if (!system.start(*parent).has_value()) {
    return 1;
}
print_children(system, *parent);  // ["child-1","child-2","child-3"]

const std::optional<xstate::actor_ref> child2 = system.child_of(*parent, "child-2");
if (!child2.has_value()) {
    return 1;
}
if (!system.send(*parent, {.type = "STOP", .payload = {}}).has_value()) {
    return 1;
}
print_children(system, *parent);  // ["child-1","child-3"]

const xstate::result<xactor::status> status = system.status_of(*child2);
if (!status.has_value()) {
    return 1;
}
std::cout << "child-2: " << name_of(*status) << '\n';

print_children, a helper of the program, writes the ids of the parent’s children. It prints:

["child-1","child-2","child-3"]
["child-1","child-3"]
child-2: stopped

The entry spawns the three children, which the parent lists in the order they were spawned. STOP stops child-2: it leaves the list, and its status is stopped.

Stopping a child

A stop_child_action stops the child its id names. The systemIds of the child’s family are released at once, or, in an initial macrostep, when the actor starts, since that macrostep defers everything to the start. The parent forgets the child, which leaves the snapshot’s children. A started child receives the stop behind everything already sent to it: it finishes the macrostep it runs and the events that arrived before, ignores what arrives after, asks its own children to stop the same way, and stops, running no exit action. A child that has already ended ignores the stop; a failed one’s children keep running, as in XState. A child spawned and stopped in the same macrostep never starts (Invoking actors). Leaving a state stops its invoked children the same way.

Sending events between actors

A machine sends an event with a send_to_action, XState’s sendTo (Sending to other actors), whose target is a string:

XState xstate’s target

sendTo('child', event)

"child" or "#_child": the child with that id

sendParent(event)

"#_parent", or a send_parent_action

sendTo('#_internal', event)

"#_internal": the actor itself, as an event of its own

sendTo(({ system }) => system.get('name'), event)

"#system:name": the actor registered under that systemId

forwardTo('child')

a forward_to_action, which sends the event being handled

The target is resolved when the action resolves, and the event is delivered once the macrostep has settled, as XState defers its sendTo. A target that names no actor fails the sender with unknown_target, where XState throws. A #system: target under which nothing is registered sends to the actor itself, as XState’s sendTo of an undefined actor does; a forward_to_action fails there with unknown_target, as XState’s development build throws.

An actor reference is not data (Targets that name actors). XState can put self in an event, and the receiver can answer event.sender; xstate’s events hold JSON, so an answer goes to #_parent, to a child’s id or to a systemId. The example is Stately’s, where the server answers the client’s sender; here the server answers its parent, the client that invoked it.

xstate::implementations server_implementations;
// The server answers the actor that sent CODE, its parent.
const xstate::send_parent_action answer{
    .event = token,
    .id = std::nullopt,
    .delay = std::nullopt,
};
server_implementations.actions.emplace("answer", answer);
const boost::json::value server_config = boost::json::parse(R"({
    "id": "server",
    "initial": "waitingForCode",
    "states": {
        "waitingForCode": {"on": {"CODE": {"actions": "answer"}}}
    }
})");
const xstate::result<xstate::machine> auth_server_machine =
    xstate::create_machine(server_config, server_implementations);
if (!auth_server_machine.has_value()) {
    return 1;
}

xstate::implementations client_implementations;
client_implementations.actors.emplace("authServer",
                                      xstate::machine_actor{*auth_server_machine});
const xstate::send_to_action send_code{
    .target = "auth-server",
    .event = code,
    .id = std::nullopt,
    .delay = std::nullopt,
};
client_implementations.actions.emplace("sendCode", send_code);
const boost::json::value client_config = boost::json::parse(R"({
    "id": "client",
    "initial": "idle",
    "states": {
        "idle": {"on": {"AUTH": {"target": "authorizing"}}},
        "authorizing": {
            "invoke": {"id": "auth-server", "src": "authServer"},
            "entry": "sendCode",
            "on": {"TOKEN": {"target": "authorized"}}
        },
        "authorized": {"type": "final"}
    }
})");
const xstate::result<xstate::machine> auth_client_machine =
    xstate::create_machine(client_config, client_implementations);
if (!auth_client_machine.has_value()) {
    return 1;
}

xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> client = system.create_actor(*auth_client_machine);
if (!client.has_value()) {
    return 1;
}
const xstate::result<void> subscribed =
    system.subscribe(*client, [](const xstate::snapshot& snapshot) {
        std::cout << boost::json::serialize(snapshot.value) << '\n';
    });
if (!subscribed.has_value()) {
    return 1;
}
if (!system.start(*client).has_value()) {
    return 1;
}
if (!system.send(*client, {.type = "AUTH", .payload = {}}).has_value()) {
    return 1;
}
// logs "idle", "authorizing", then "authorized"

It prints:

"idle"
"authorizing"
"authorized"

AUTH takes the client to authorizing, which invokes the server and, on entry, sends it CODE. The entry’s send names the state’s own invoke, which does not exist yet when the entry action is resolved; the event waits until the child has started, as in XState. The server answers TOKEN, which takes the client to authorized. Stately’s server answers after a second, with { delay: 1000 }; with .delay = xstate::delay_ref{std::uint64_t{1000}}, the token arrives when the host brings the time to 1000 (Delayed events and time).

Each actor receives what one sender sends it in the order sent. Between senders the order can differ from XState’s, which delivers an event to an idle actor at once, nested in the sender’s call, where xactor queues every delivery (Interleaving across actors).

The system

All the actors of an actor_system belong to one system. An actor registered under a systemId can be found by any other: an invoke’s systemId, a spawn’s system_id and actor_options::system_id register one. From the host, get(system_id) finds it, XState’s system.get; from a machine, a send’s target #system:<systemId> names it, where XState writes ({ system }) => system.get(id), a function JSON cannot hold. The example is the one of Stately’s page on systems: a feedback machine invokes a form and a notifier, each under a systemId, and the form notifies the notifier by name. That page’s machine registers no notifier; this one invokes one, so the event has somewhere to go.

xstate::result<xstate::event> notification(const xstate::action_args& /*args*/) {
    return xstate::event{.type = "notify", .payload = {{"message", "Form submitted!"}}};
}

/** {last: event.message} */
xstate::result<boost::json::object> keep_last(const xstate::action_args& args) {
    const boost::json::value* message = args.event.payload.if_contains("message");
    if (message == nullptr) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"last", *message}};
}
xstate::implementations notifier_implementations;
notifier_implementations.actions.emplace("keep",
                                         xstate::assign_action{.assignment = keep_last});
const boost::json::value notifier_config = boost::json::parse(R"({
    "context": {"last": null},
    "on": {"notify": {"actions": "keep"}}
})");
const xstate::result<xstate::machine> notifier_machine =
    xstate::create_machine(notifier_config, notifier_implementations);
if (!notifier_machine.has_value()) {
    return 1;
}

xstate::implementations form_implementations;
const xstate::send_to_action notify{
    .target = "#system:notifier",
    .event = notification,
    .id = std::nullopt,
    .delay = std::nullopt,
};
form_implementations.actions.emplace("notify", notify);
const boost::json::value form_config = boost::json::parse(R"({
    "on": {"submit": {"actions": "notify"}}
})");
const xstate::result<xstate::machine> form_machine =
    xstate::create_machine(form_config, form_implementations);
if (!form_machine.has_value()) {
    return 1;
}

xstate::implementations feedback_implementations;
feedback_implementations.actors.emplace("formMachine", xstate::machine_actor{*form_machine});
feedback_implementations.actors.emplace("notifierMachine",
                                        xstate::machine_actor{*notifier_machine});
const boost::json::value feedback_config = boost::json::parse(R"({
    "invoke": [
        {"systemId": "formMachine", "src": "formMachine"},
        {"systemId": "notifier", "src": "notifierMachine"}
    ]
})");
const xstate::result<xstate::machine> feedback_machine =
    xstate::create_machine(feedback_config, feedback_implementations);
if (!feedback_machine.has_value()) {
    return 1;
}

xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> feedback_actor = system.create_actor(*feedback_machine);
if (!feedback_actor.has_value()) {
    return 1;
}
if (!system.start(*feedback_actor).has_value()) {
    return 1;
}
const std::optional<xstate::actor_ref> form = system.get("formMachine");
if (!form.has_value()) {
    return 1;
}
if (!system.send(*form, {.type = "submit", .payload = {}}).has_value()) {
    return 1;
}
const std::optional<xstate::actor_ref> notifier = system.get("notifier");
if (!notifier.has_value()) {
    return 1;
}
const xstate::snapshot* notified = system.snapshot_of(*notifier);
if (notified == nullptr) {
    return 1;
}
std::cout << boost::json::serialize(notified->context) << '\n';
// logs {"last":"Form submitted!"}

It prints:

{"last":"Form submitted!"}

A systemId names its actor from the actor’s creation until it is done, failed or stopped, or until a stopChild of it or of an ancestor releases it. A claim of a name another actor holds fails the claiming actor with system_id_taken, as XState throws; create_actor returns the error instead.

The roots of one actor_system share one system: their systemIds, get and the #system: targets. XState’s createActor makes a system for each root; a host that wants that creates an actor_system for each root (One system per actor_system). The host stops a system by stopping its roots: stop refuses a child.

Delayed events and time

Every after and every delayed built-in action that sends arms a timer (Delayed raise, ids and cancel): a delayed raise_action, send_to_action, send_parent_action or forward_to_action. The timer is owned by the actor that armed it, under the action’s id when it has one, and a cancel_action of that id removes it. Only built-in actions arm one: a custom action goes to on_action, whatever its type or params. The machine never reads a clock, and neither does the system: the host brings the time. clock_tick(now) moves the system’s clock to now, in milliseconds, and releases the timers due by then one at a time, by deadline and then by arming order, running the system after each. It is XState’s SimulatedClock, passed to createActor as its clock, and its set(now). The machine core’s simulated_clock is the clock of a machine run without actors: it schedules only the built-in delayed raises a step returns, and leaves a custom action to its caller, whatever its type or params (Delayed events and the simulated clock). A timer is due at the time of the last tick, 0 before any, plus its delay. The example is the delayed send and cancel of Stately’s page on actions. That page does not define someActor; here it is a child the machine invokes, which counts what it receives.

xstate::implementations some_actor_implementations;
some_actor_implementations.actions.emplace("count",
                                           xstate::assign_action{.assignment = increment});
const boost::json::value some_actor_config = boost::json::parse(R"({
    "context": {"count": 0},
    "on": {"someEvent": {"actions": "count"}}
})");
const xstate::result<xstate::machine> some_actor =
    xstate::create_machine(some_actor_config, some_actor_implementations);
if (!some_actor.has_value()) {
    return 1;
}

xstate::implementations implementations;
implementations.actors.emplace("someActor", xstate::machine_actor{*some_actor});
const xstate::send_to_action transmit{
    .target = "someActor",
    .event = some_event,
    .id = "someId",
    .delay = xstate::delay_ref{std::uint64_t{1000}},
};
implementations.actions.emplace("transmit", transmit);
implementations.actions.emplace("cancelIt", xstate::cancel_action{.id = "someId"});
const boost::json::value config = boost::json::parse(R"({
    "invoke": {"id": "someActor", "src": "someActor"},
    "on": {
        "event": {"actions": "transmit"},
        "cancelEvent": {"actions": "cancelIt"}
    }
})");
const xstate::result<xstate::machine> machine = xstate::create_machine(config, implementations);
if (!machine.has_value()) {
    return 1;
}
xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> actor = system.create_actor(*machine);
if (!actor.has_value()) {
    return 1;
}
if (!system.start(*actor).has_value()) {
    return 1;
}
// At 0 ms: armed for 1000 ms, then cancelled.
if (!system.send(*actor, {.type = "event", .payload = {}}).has_value()) {
    return 1;
}
if (!system.send(*actor, {.type = "cancelEvent", .payload = {}}).has_value()) {
    return 1;
}
if (!system.clock_tick(1000).has_value()) {
    return 1;
}
print_count(system, *actor);  // count 0

// At 1000 ms: armed for 2000 ms.
if (!system.send(*actor, {.type = "event", .payload = {}}).has_value()) {
    return 1;
}
if (!system.clock_tick(1999).has_value()) {
    return 1;
}
print_count(system, *actor);  // count 0
if (!system.clock_tick(2000).has_value()) {
    return 1;
}
print_count(system, *actor);  // count 1

print_count, a helper of the program, writes the count in the context of the child someActor. It prints:

count 0
count 0
count 1

At 0 ms, event arms a timer for 1000 ms under the id someId, and cancelEvent removes it, so nothing arrives at 1000. At 1000 ms, event arms another, due at 2000: at 1999 it has not fired, and at 2000 it has.

The rules of time:

  • Timers fire one at a time, so the handling of one can cancel a later one of the same tick, as XState’s SimulatedClock fires them. A timer armed during the tick and already due fires in the same call (A timer that falls due during clock_tick).

  • The timers of an actor that has stopped, is done or has failed never fire (Timers of an ended actor).

  • Time is whole milliseconds, where XState’s SimulatedClock takes fractions (Time in whole milliseconds).

  • Bring the time forward only. clock_tick does not refuse a time before the last one, and a timer armed after it counts from the earlier time; XState’s set throws for it, and the core’s simulated_clock refuses it with invalid_config (A tick earlier than the last).

Host actors

A host actor is a child whose work the host does: a request to a server, a file to read, anything that waits on the world outside the machines. It stands for XState’s promise actor, fromPromise, which runs asynchronous work and resolves with an output or rejects with an error. A C++ library with no event loop and no thread cannot await a promise; the host, which owns the I/O, does that work, at its own pace, and answers. A host_actor in implementations.actors registers one. Once started, a host actor is a pending request, a host_request:

Member What it holds

actor

The host actor.

parent

The actor that invoked or spawned it.

id

Its id, as its parent knows it.

src

The name it is registered under.

input

Its input; none for JavaScript’s undefined, when it was given none.

host_requests() lists the pending requests, in the order their actors started. resolve(request, output) ends the host actor done: its parent receives xstate.done.actor.<id> with the output, or without one when the output is none. reject(request, error) ends it failed: its parent receives xstate.error.actor.<id> with the error. Both run the system, as send does. The example is Stately’s user machine, whose promise fetches a user; the host rejects the first request and resolves the second. The invoke’s input, the user id of the context, is computed by user_id_of, registered under the invoke’s id getUser (An input computed from the context and the event), and the host reads it from the request.

/** The input of the invoke getUser: {userId} from the context. */
xstate::result<std::optional<boost::json::value>> user_id_of(const xstate::action_args& args) {
    const boost::json::object* context = args.context.if_object();
    const boost::json::value* user_id =
        context == nullptr ? nullptr : context->if_contains("userId");
    if (user_id == nullptr) {
        return xstate::failure<std::optional<boost::json::value>>(
            xstate::errc::implementation_failed);
    }
    return std::optional<boost::json::value>(boost::json::object{{"userId", *user_id}});
}

/** {user: event.output} */
xstate::result<boost::json::object> assign_user(const xstate::action_args& args) {
    const boost::json::value* output = args.event.payload.if_contains("output");
    if (output == nullptr) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"user", *output}};
}

/** {error: event.error} */
xstate::result<boost::json::object> assign_error(const xstate::action_args& args) {
    const boost::json::value* error = args.event.payload.if_contains("error");
    if (error == nullptr) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"error", *error}};
}
xstate::implementations implementations;
// The promise is the host's work: a host actor.
implementations.actors.emplace("fetchUser", xstate::host_actor{});
implementations.inputs.emplace("getUser", user_id_of);
implementations.actions.emplace("assignUser", xstate::assign_action{.assignment = assign_user});
implementations.actions.emplace("assignError",
                                xstate::assign_action{.assignment = assign_error});
const boost::json::value config = boost::json::parse(R"({
    "id": "user",
    "initial": "idle",
    "context": {"userId": "42", "user": null, "error": null},
    "states": {
        "idle": {"on": {"FETCH": {"target": "loading"}}},
        "loading": {
            "invoke": {
                "id": "getUser",
                "src": "fetchUser",
                "onDone": {"target": "success", "actions": "assignUser"},
                "onError": {"target": "failure", "actions": "assignError"}
            }
        },
        "success": {},
        "failure": {"on": {"RETRY": {"target": "loading"}}}
    }
})");
const xstate::result<xstate::machine> user_machine =
    xstate::create_machine(config, implementations);
if (!user_machine.has_value()) {
    return 1;
}
xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> actor = system.create_actor(*user_machine);
if (!actor.has_value()) {
    return 1;
}
if (!system.start(*actor).has_value()) {
    return 1;
}
if (!system.send(*actor, {.type = "FETCH", .payload = {}}).has_value()) {
    return 1;
}
std::vector<xstate::host_request> requests = system.host_requests();
print_requests(requests);  // request fetchUser getUser {"userId":"42"}
if (requests.empty()) {
    return 1;
}
if (!system.reject(requests.front(), "Network error").has_value()) {
    return 1;
}
print(system, *actor);  // "failure", with the error

if (!system.send(*actor, {.type = "RETRY", .payload = {}}).has_value()) {
    return 1;
}
requests = system.host_requests();
print_requests(requests);  // a new request
if (requests.empty()) {
    return 1;
}
const boost::json::object user{{"name", "David"}, {"location", "Florida"}};
if (!system.resolve(requests.front(), user).has_value()) {
    return 1;
}
print(system, *actor);  // "success", with the user

print_requests, a helper of the program, writes each pending request’s src, id and input, and print the state value and the context. It prints:

request fetchUser getUser {"userId":"42"}
"failure" {"userId":"42","user":null,"error":"Network error"}
request fetchUser getUser {"userId":"42"}
"success" {"userId":"42","user":{"name":"David","location":"Florida"},"error":"Network error"}

FETCH enters loading, whose invoke starts the host actor, and its request appears. The rejection takes the machine to failure, with the error in its context; RETRY enters loading again, which invokes a new host actor with a new request, and its resolution takes the machine to success, with the user. The context starts with null for the user and the error: Stately’s page leaves them undefined, and JSON has no undefined.

A host actor takes no event, as XState’s promise actor ignores them. A request ends with its actor: when the invoking state is left, the host actor is stopped and its request is no longer pending, and resolve or reject of it returns xactor’s invalid_argument and changes nothing, as XState ignores a promise that settles after its actor stopped. A host actor is always a child: create_actor takes a machine.

Fuel, parking and resume

XState runs each call to its end: send returns when every macrostep it caused has settled, however long that takes, and a call that enters a cycle of eventless transitions never returns. A host that runs machines it does not trust, or must answer within a budget, cannot allow that. xstate bounds the work of each call by fuel.

The fuel of the budgets given to the constructor (Creating an actor), a million by default, is the fuel of one execution. An execution pays one unit for each microstep, before it runs, and one for each message an actor sends: an event, a child’s construction and the answer to it, a child’s start, a stop. Each timer an actor arms costs one too. Each call that delivers opens executions of its own, each with the whole budget: one for each input it applies, and one for each parked actor resume() continues. The timers a clock_tick releases all share its one execution, so the budget also bounds timers that keep arming within one tick. The start of a machine that invokes one machine child costs five: the root’s initial microstep, the child’s construction, its initial microstep, its answer and its start.

When an execution cannot pay, the actor parks: it keeps its cursor, the actions it has not resolved and the effects it has not run, and the call returns out_of_fuel. A parked actor runs nothing it has not paid for, and nothing twice. An event that reaches it waits behind what it has left, so run to completion holds: no event starts before the macrostep in progress has settled. resume() gives each parked actor a new execution and continues it where it stopped. run_until_idle() delivers nothing: it runs what is queued and returns the same outcome. Every call above already ends with it, so after one of them it only reports whether an actor is still parked.

While an actor is parked, the host’s inputs wait: start, send, clock_tick, resolve and reject are checked, then held in the order made, and return out_of_fuel. resume() applies them once the parked actors have settled, each run to its end before the next, as XState runs each call to its end. stop is never held: it is how the host ends an actor that would never settle. The example runs a chain of eventless transitions on a fuel of 3, then a cycle that never settles; it has no twin, since XState has no fuel.

// GO takes five microsteps: idle to a, then four eventless ones.
const boost::json::value chain_config = boost::json::parse(R"({
    "initial": "idle",
    "states": {
        "idle": {"on": {"GO": "a"}},
        "a": {"always": "b"},
        "b": {"always": "c"},
        "c": {"always": "d"},
        "d": {"always": "e"},
        "e": {"on": {"PING": "f"}},
        "f": {}
    }
})");
const xstate::result<xstate::machine> chain = xstate::create_machine(chain_config, {});
if (!chain.has_value()) {
    return 1;
}

xstate::actor_system system({.fuel = 3});
const xstate::result<xstate::actor_ref> actor = system.create_actor(*chain);
if (!actor.has_value()) {
    return 1;
}
const xstate::result<xstate::run_outcome> started = system.start(*actor);
if (!started.has_value()) {
    return 1;
}
print("start", *started, system, *actor);  // start: settled "idle"

const xstate::result<xstate::run_outcome> go =
    system.send(*actor, {.type = "GO", .payload = {}});
if (!go.has_value()) {
    return 1;
}
print("GO", *go, system, *actor);  // GO: out_of_fuel "idle"

const xstate::result<xstate::run_outcome> ping =
    system.send(*actor, {.type = "PING", .payload = {}});
if (!ping.has_value()) {
    return 1;
}
print("PING", *ping, system, *actor);  // PING: out_of_fuel "idle"

const xstate::result<xstate::run_outcome> resumed = system.resume();
if (!resumed.has_value()) {
    return 1;
}
print("resume", *resumed, system, *actor);  // resume: settled "f"
// An eventless cycle: its initial macrostep never settles.
const boost::json::value spin_config = boost::json::parse(R"({
    "initial": "ping",
    "states": {
        "ping": {"always": "pong"},
        "pong": {"always": "ping"}
    }
})");
const xstate::result<xstate::machine> spin = xstate::create_machine(spin_config, {});
if (!spin.has_value()) {
    return 1;
}

xstate::actor_system bounded({.fuel = 100});
const xstate::result<xstate::actor_ref> spinner = bounded.create_actor(*spin);
if (!spinner.has_value()) {
    return 1;
}
const xstate::result<xstate::run_outcome> spun = bounded.start(*spinner);
if (!spun.has_value()) {
    return 1;
}
print("start", *spun, bounded, *spinner);  // start: out_of_fuel none
const xstate::result<xstate::run_outcome> again = bounded.resume();
if (!again.has_value()) {
    return 1;
}
print("resume", *again, bounded, *spinner);  // resume: out_of_fuel none

if (!bounded.stop(*spinner).has_value()) {
    return 1;
}
const xstate::result<xactor::status> status = bounded.status_of(*spinner);
if (!status.has_value()) {
    return 1;
}
std::cout << "stop: " << name_of(*status) << '\n';  // stop: stopped

print, a helper of the program, writes the call, the outcome it returned and the state value the actor has published. It prints:

start: settled "idle"
GO: out_of_fuel "idle"
PING: out_of_fuel "idle"
resume: settled "f"
start: out_of_fuel none
resume: out_of_fuel none
stop: stopped

With a fuel of 3, the five microsteps of GO cannot be paid in one call: the actor parks after three, its snapshot still the one from before GO, and PING waits behind it. resume() pays the last two, then runs PING. The second machine’s initial macrostep never settles: its start and each resume() spend a budget of 100 and park it again, before it has published a snapshot, until stop ends it. XState has no fuel (Appendix B, Fuel): it returns from send({ type: 'GO' }) with the actor in "e", and never returns from the createActor of the second machine, since it runs the initial macrostep when it constructs the actor (The root is constructed on start). The macrostep cursor bounds a single machine the same way, one microstep for each call of next() (the cursor’s fuel).

How the layer runs

  • One thread, queued deliveries. The system runs on one xactor scheduler, on the host’s thread (xactor’s model). Each actor has a first-in first-out mailbox, and the actors that have work wait in one first-in first-out queue: a turn takes the actor at its head and hands it the first message of its mailbox, and the actor goes back to the end of the queue while its mailbox holds more. So messages to different actors are served actor by actor, in turns, and can run in another order than the one they were sent in. Nothing runs between two calls of the host, and the same calls give the same run: the order of the events, the callbacks and the timers depends on the inputs alone.

  • Run to completion, per actor. An actor runs one macrostep at a time; an event that arrives while one has not settled waits, and the waiting events run in the order they arrived, each a macrostep of its own.

  • Interleaving. XState delivers an event to an idle actor at once, nested in its sender’s call; xactor queues it. Within one actor the order is XState’s. Across actors it can differ, and so can the order of the callbacks: a parent handles the last sends of a child that is done after the child has released its systemId, and an actor’s start hands over its own initial custom actions before its initial children hand over theirs (Interleaving across actors).

  • Errors are values. No call throws: a refusal is an error code, and an actor that fails says so in its snapshot and to its parent (Failures are values).

  • The host owns the effects. Custom actions, logs, emitted events, snapshots, the work of host actors and time cross between the system and the host as data, through its calls and callbacks, and nothing else does.

Where the layer differs from XState

Within one actor, the layer keeps XState’s order. What differs, between actors and around the host’s calls, follows from running on xactor: fuel and parking (Appendix B, Fuel), deliveries queued instead of nested, a root constructed on start, one system for every root of an actor_system, time in whole milliseconds that the host brings, and callbacks that run inside an actor’s turn. Appendix B gives each with what XState does and why, in What the actor model on xactor imposes, which also names the entries of its other groups that concern actors.

Testing

A test checks that a machine’s logic does what it should: from a known state, given some events, it reaches the states it should and asks for the effects it should. Stately’s testing guide writes each test in three parts, and so does this chapter:

  • Arrange: create the machine, and the actor that runs it when the test needs one.

  • Act: send it events.

  • Assert: check the states it reached and the effects it asked for.

A test is worth its assertions only when the run it checks is the same every time. In xstate nothing in a run depends on the world outside the test:

  • There is no real clock. A delay waits on a clock the test owns, and only the test moves its time (Time).

  • There is no thread and no promise. A pure function returns when its macrostep has settled; a call of the actor system returns when its work is done, or says that it ran out of fuel; the work a promise would do is asked of the host, which the test plays (Mocking effects).

  • There is no exception. An implementation that fails puts the snapshot in error, where the test reads it (Testing failures).

  • A custom action comes back as data, with its params, so the test reads it where XState’s test registers a mock (Testing a machine).

  • Work is counted. A test can bound the microsteps an event may take, and run an actor system with every budget of fuel (Bounding a macrostep, Every fuel budget).

A machine is tested at two levels. With the machine core, <webcpp/xstate.hpp>, a test calls it as a function, a snapshot and an event in, the next snapshot and its actions out, through XState’s pure functions or the macrostep cursor (Pure transitions and the macrostep cursor). With the actor layer, <webcpp/xstate/actors.hpp>, a test runs machines as actors, with their children, their timers and their requests to the host (Actors). A program that uses xactor alone, without machines, tests its actors as xactor’s page shows. The last part of the chapter tells how xstate itself is tested, against XState (How xstate is tested).

The examples are programs that print what they check and return 1 when a check fails, which fails b2 libs/xstate/example (The examples), and each shows its whole main. In a test framework each check is an assertion; xstate’s own tests use Boost.Core’s lightweight_test, and Boost.Test for the suites that run the ported cases (The tests).

Testing a machine

The shortest test of a machine uses its pure functions: get_initial_snapshot returns the snapshot the machine starts in, and transition takes a snapshot and an event and returns the next snapshot with the actions its macrostep returned. No actor is created, and nothing runs between two calls. Stately’s first example tests a toggle that notifies each time it becomes active. On the pure functions, the test reads:

int main() {
    // 1. Arrange
    const boost::json::value config = boost::json::parse(R"({
        "initial": "inactive",
        "states": {
            "inactive": {"on": {"toggle": {"target": "active"}}},
            "active": {
                "entry": {"type": "notify", "params": {"message": "Active!"}},
                "on": {"toggle": {"target": "inactive"}}
            }
        }
    })");
    const xstate::result<xstate::machine> machine = xstate::create_machine(config, {});
    if (!machine.has_value()) {
        return 1;
    }

    // 2. Act
    const xstate::event toggle{.type = "toggle", .payload = {}};
    xstate::snapshot current = xstate::get_initial_snapshot(*machine);
    boost::json::array notified_messages;
    for (int sent = 0; sent < 3; ++sent) {
        const auto [next, actions] = xstate::transition(*machine, current, toggle);
        for (const xstate::action& returned : actions) {
            const boost::json::value* message =
                returned.params.is_object() ? returned.params.get_object().if_contains("message")
                                            : nullptr;
            if (returned.type == "notify" && message != nullptr) {
                notified_messages.push_back(*message);
            }
        }
        current = next;
    }

    // 3. Assert
    const boost::json::value expected = boost::json::parse(R"(["Active!", "Active!"])");
    if (current.value != boost::json::value("active") ||
        boost::json::value(notified_messages) != expected) {
        return 1;
    }
    std::cout << "value: " << boost::json::serialize(current.value) << '\n';
    std::cout << "notified: " << boost::json::serialize(notified_messages) << '\n';
    return 0;
}

It prints:

value: "active"
notified: ["Active!","Active!"]

notify has no implementation, so it is a custom action, and transition returns it to its caller with its params, {"message": "Active!"}. The test is that caller, and keeps the message of each notify it is returned, where Stately’s test registers notify as a mock, a function that records the params it is called with. A function cannot be JSON, so a config names its actions and leaves the custom ones to the code that runs the machine (Custom actions); a test is such code, and needs no mock. XState’s own transition returns its custom actions the same way. An assign is the one action never returned: it changes the context, and the test asserts on snapshot.context instead.

get_initial_snapshot, transition and the other pure functions run each macrostep to its end, without a bound: on a machine whose eventless transitions never settle, they never return. A test of such a machine drives the cursor itself (Bounding a macrostep).

Testing failures

An implementation can fail: an assign handed an event without the member it needs, a guard that cannot decide. Where XState’s implementation throws, xstate’s returns an error, a result holding an error code: xstate throws nothing, and reports every error as a value. The macrostep then settles at once, with the snapshot it began from, its status error and its error the code the implementation returned. That is the snapshot XState’s actor keeps when an implementation throws, while XState’s pure functions throw instead (Failures are values). So a test of a failure needs no actor. save_age fails when the event carries no age, and the test sends save without one:

/** Keeps the event's age, and fails when the event carries none. */
xstate::result<boost::json::object> save_age(const xstate::action_args& args) {
    const boost::json::value* age = args.event.payload.if_contains("age");
    if (age == nullptr || !age->is_number()) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"age", *age}};
}

int main() {
    // 1. Arrange
    xstate::implementations implementations;
    implementations.actions.emplace("saveAge", xstate::assign_action{.assignment = save_age});
    const boost::json::value config = boost::json::parse(R"({
        "initial": "editing",
        "context": {"age": 0},
        "states": {
            "editing": {"on": {"save": {"target": "saved", "actions": "saveAge"}}},
            "saved": {}
        }
    })");
    const xstate::result<xstate::machine> machine = xstate::create_machine(config, implementations);
    if (!machine.has_value()) {
        return 1;
    }

    // 2. Act
    const xstate::snapshot editing = xstate::get_initial_snapshot(*machine);
    const xstate::event save{.type = "save", .payload = {}};
    const xstate::snapshot failed = xstate::get_next_snapshot(*machine, editing, save);

    // 3. Assert
    if (failed.status != xstate::status::error ||
        failed.error != xstate::errc::implementation_failed ||
        failed.value != boost::json::value("editing")) {
        return 1;
    }
    std::cout << "status: error\n";
    std::cout << "value: " << boost::json::serialize(failed.value) << '\n';
    std::cout << "context: " << boost::json::serialize(failed.context) << '\n';
    return 0;
}

It prints:

status: error
value: "editing"
context: {"age":0}

The machine stays where it was before save: in editing, with its context unchanged. The snapshot holds the error_code the implementation returned, here implementation_failed, where XState’s actor holds the thrown Error. An implementation may return any error_code, so a test can tell its own failures apart.

An actor of xstate fails the same way, so the test carries over to the actor layer: after a send that fails, status_of(actor) holds xactor::status::error, and snapshot_of(actor) has the status error and the error code (Snapshots and status). The test reads the failure there: xstate has no error callback, and a listener subscribe registers is not handed a failed snapshot (Watching an actor). A parent hears of a child’s failure through the child’s error event (Done and error events).

Every microstep

A macrostep is everything one event causes: the transition the event selects, then every eventless transition and raised event that follows, each one microstep, until the machine is stable. A snapshot shows where the macrostep ended, and nothing of the way there. A state entered and left by an eventless (always) transition in one macrostep never appears in a snapshot: XState’s subscribers and getSnapshot() never see it, and neither does a test that asserts on the snapshot. Stately’s page suggests listening to the inspection API’s @xstate.microstep events, or changing the machine so that a zero delay replaces the always.

xstate’s cursor shows each microstep without changing the machine (The macrostep cursor). begin starts the macrostep an event runs, and each call of next() runs exactly one microstep and returns a progress: the microstep, with the snapshot it left and the actions it returned, and settled, true on the microstep that ends the macrostep and on no other. In this machine submit enters validating, which an eventless transition leaves at once:

int main() {
    const boost::json::value config = boost::json::parse(R"({
        "initial": "idle",
        "states": {
            "idle": {"on": {"submit": "validating"}},
            "validating": {"entry": "validate", "always": "submitted"},
            "submitted": {}
        }
    })");
    const xstate::result<xstate::machine> machine = xstate::create_machine(config, {});
    if (!machine.has_value()) {
        return 1;
    }

    const xstate::snapshot initial = xstate::get_initial_snapshot(*machine);
    const xstate::event submit{.type = "submit", .payload = {}};
    xstate::macrostep step = xstate::begin(*machine, initial, submit);
    std::vector<boost::json::value> values;
    while (!step.done()) {
        const xstate::progress made = step.next();
        values.push_back(made.step.snapshot.value);
        std::cout << boost::json::serialize(made.step.snapshot.value) << ' '
                  << types_of(made.step.actions) << (made.settled ? " settled" : "") << '\n';
    }

    // The transient state was entered, then left in the same macrostep.
    const std::vector<boost::json::value> expected{"validating", "submitted"};
    if (values != expected) {
        return 1;
    }
    return 0;
}

It prints:

"validating" [validate]
"submitted" [] settled

submit entered validating, which returned its entry action validate, and the eventless transition left it in the same macrostep, which settled in submitted. get_microsteps, XState’s getMicrosteps, is the same cursor run to its end: it returns the microsteps as a list, whose last one settles, for a test that needs the list only (The microsteps of an event).

Bounding a macrostep

A cycle of eventless transitions never reaches a stable state. XState runs it until the config’s options.maxIterations, when one is set, makes the macrostep throw; by default there is no bound, and the call never returns (A macrostep that never settles). xstate never stops a macrostep on its own, and does not read that option: the caller that drives the cursor pays for each microstep and decides how many it allows, which is its fuel (Fuel). A test uses the same bound to assert that an event settles within a number of microsteps, and to report a cycle as a failure instead of hanging on it.

In this machine GO takes three microsteps, through two states that eventless transitions leave at once, and SPIN enters a cycle that never settles. settle_within runs at most fuel microsteps of a macrostep, and returns its result only when it has settled:

/**
 Runs at most `fuel` microsteps of a macrostep: its result once it has
 settled, nothing when the fuel ran out first, the cursor left where it
 stopped for its caller to resume or drop.
*/
std::optional<xstate::macrostep_result> settle_within(xstate::macrostep& step, std::size_t fuel) {
    for (std::size_t paid = 0; paid < fuel && !step.done(); ++paid) {
        step.next();
    }
    if (!step.done()) {
        return std::nullopt;
    }
    return step.result();
}

int main() {
    const boost::json::value config = boost::json::parse(R"({
        "initial": "idle",
        "states": {
            "idle": {"on": {"GO": "one", "SPIN": "ping"}},
            "one": {"entry": "first", "always": "two"},
            "two": {"entry": "second", "always": "three"},
            "three": {"entry": "third"},
            "ping": {"always": "pong"},
            "pong": {"always": "ping"}
        }
    })");
    const xstate::result<xstate::machine> machine = xstate::create_machine(config, {});
    if (!machine.has_value()) {
        return 1;
    }
    const xstate::snapshot idle = xstate::get_initial_snapshot(*machine);

    const xstate::event go{.type = "GO", .payload = {}};
    xstate::macrostep going = xstate::begin(*machine, idle, go);
    const std::optional<xstate::macrostep_result> went = settle_within(going, 10);
    if (!went.has_value() || went->snapshot.value != boost::json::value("three")) {
        return 1;
    }
    std::cout << "GO settles in " << went->microsteps << " microsteps at "
              << boost::json::serialize(went->snapshot.value) << " with " << types_of(went->actions)
              << '\n';

    const xstate::event spin{.type = "SPIN", .payload = {}};
    xstate::macrostep spinning = xstate::begin(*machine, idle, spin);
    if (settle_within(spinning, 1000).has_value()) {
        return 1;
    }
    std::cout << "SPIN has not settled after 1000 microsteps\n";
    return 0;
}

It prints:

GO settles in 3 microsteps at "three" with [first, second, third]
SPIN has not settled after 1000 microsteps

The test learns of the cycle from a cursor that has not settled when its fuel is spent, where XState’s learns of it from the error its bound throws. The test holds that cursor, spinning, by name: it is still there, where it stopped, to resume or to drop.

Resuming a cursor

A cursor is a value. A caller that runs out of fuel keeps it, or a copy of it, and resumes it later, perhaps in a later request the same process serves; the macrostep goes on exactly where it stopped, with the raised events it had queued. A test of code that pays fuel checks that, wherever it stops, the cursor and a copy of it settle where a run without a stop settles. XState has no cursor, so this test has no twin (The XState twins). finish runs a cursor to its end, and same compares two settled macrosteps:

/** Runs the microsteps a cursor has left; the macrostep it settles. */
xstate::macrostep_result finish(xstate::macrostep& cursor) {
    while (!cursor.done()) {
        cursor.next();
    }
    return cursor.result();
}

/** Whether two settled macrosteps agree: microsteps, snapshot and actions. */
bool same(const xstate::macrostep_result& left, const xstate::macrostep_result& right) {
    return left.microsteps == right.microsteps && left.snapshot.value == right.snapshot.value &&
           left.snapshot.context == right.snapshot.context &&
           types_of(left.actions) == types_of(right.actions);
}

The test stops a cursor after every number of microsteps from 0 to the macrostep’s length, copies it there, and finishes both:

xstate::macrostep straight = xstate::begin(*machine, idle, go);
const xstate::macrostep_result expected = finish(straight);

for (std::size_t stopped = 0; stopped <= expected.microsteps; ++stopped) {
    xstate::macrostep original = xstate::begin(*machine, idle, go);
    for (std::size_t paid = 0; paid < stopped; ++paid) {
        original.next();
    }
    xstate::macrostep copy = original;
    const xstate::macrostep_result from_copy = finish(copy);
    const xstate::macrostep_result from_original = finish(original);
    if (!same(from_copy, expected) || !same(from_original, expected)) {
        return 1;
    }
    std::cout << "stopped after " << stopped << ", the cursor and its copy settle at "
              << boost::json::serialize(from_copy.snapshot.value) << " with "
              << types_of(from_copy.actions) << '\n';
}

It prints:

stopped after 0, the cursor and its copy settle at "three" with [first, second, third]
stopped after 1, the cursor and its copy settle at "three" with [first, second, third]
stopped after 2, the cursor and its copy settle at "three" with [first, second, third]
stopped after 3, the cursor and its copy settle at "three" with [first, second, third]

Time

A delayed transition, after, waits for time to pass. A test that waits for real time is slow, and fails when the machine running it is slower still, so the test brings the time, as XState’s tests do with a SimulatedClock. xstate’s actor system reads no real clock: clock_tick(now) moves the system’s clock to now, in milliseconds, and the timers due by then fire one at a time. Like the set(now) of XState’s SimulatedClock, and unlike its increment(ms), it takes the time to move to, not an increment, so a second tick 510 ms after a first at 500 is clock_tick(1010) (Delayed events and time). The test runs a traffic light and checks it at 500 ms and at 1010 ms:

/** Moves the system's clock to `now`; whether the actor is then in `expected`, which it prints. */
bool tick(xstate::actor_system& system, xstate::actor_ref actor, std::uint64_t now,
          std::string_view expected) {
    if (system.clock_tick(now) != xstate::run_outcome::settled) {
        return false;
    }
    const xstate::snapshot* current = system.snapshot_of(actor);
    if (current == nullptr) {
        return false;
    }
    std::cout << "after " << now << " ms: " << boost::json::serialize(current->value) << '\n';
    return current->value == boost::json::value(expected);
}

int main() {
    const boost::json::value config = boost::json::parse(R"({
        "id": "light",
        "initial": "green",
        "states": {
            "green": {"after": {"1000": "yellow"}},
            "yellow": {"after": {"1000": "red"}},
            "red": {"after": {"1000": "green"}}
        }
    })");
    const xstate::result<xstate::machine> machine = xstate::create_machine(config, {});
    if (!machine.has_value()) {
        return 1;
    }

    xstate::actor_system system({.fuel = 10'000});
    const xstate::result<xstate::actor_ref> actor = system.create_actor(*machine);
    if (!actor.has_value() || system.start(*actor) != xstate::run_outcome::settled) {
        return 1;
    }

    if (!tick(system, *actor, 500, "green") || !tick(system, *actor, 1010, "yellow")) {
        return 1;
    }
    return 0;
}

It prints:

after 500 ms: "green"
after 1010 ms: "yellow"

The light and the test are those of the first test of XState’s after.test.ts, with a simulated clock where XState’s test uses Vitest’s fake timers; xstate’s cases port that test (The ported cases). Time is counted in whole milliseconds, where XState’s clock takes fractions (Time in whole milliseconds).

Without the actor layer, a delay is data. The machine never reads a clock: entering a state with after returns a built-in delayed xstate.raise, and leaving it returns a built-in xstate.cancel. A simulated_clock the test owns takes every action a step returns (apply): it schedules a built-in delayed raise, applies a built-in cancel, and leaves a custom action to the test whatever its type or params. The test moves the time by a duration (increment, as XState’s increment), and pop_due releases each event whose time has come, which the test sends to the machine as a macrostep of its own (Delayed events and the simulated clock). The same test, on the pure functions:

/**
 Gives the clock every action a macrostep returned: it schedules the built-in
 delayed raises and applies the built-in cancels.
*/
void schedule(xstate::simulated_clock& clock, const std::vector<xstate::action>& actions) {
    for (const xstate::action& returned : actions) {
        clock.apply(returned);
    }
}

/**
 Moves the clock forward and delivers each event that comes due, as a
 macrostep of its own, scheduling what that macrostep returns.
*/
void advance(const xstate::machine& machine, xstate::snapshot& current,
             xstate::simulated_clock& clock, std::uint64_t milliseconds) {
    clock.increment(milliseconds);
    while (std::optional<xstate::event> due = clock.pop_due()) {
        auto [next, actions] = xstate::transition(machine, current, *due);
        schedule(clock, actions);
        current = std::move(next);
    }
}

int main() {
    const boost::json::value config = boost::json::parse(R"({
        "id": "light",
        "initial": "green",
        "states": {
            "green": {"after": {"1000": "yellow"}},
            "yellow": {"after": {"1000": "red"}},
            "red": {"after": {"1000": "green"}}
        }
    })");
    const xstate::result<xstate::machine> machine = xstate::create_machine(config, {});
    if (!machine.has_value()) {
        return 1;
    }

    xstate::simulated_clock clock;
    auto [current, actions] = xstate::initial_transition(*machine);
    schedule(clock, actions);

    advance(*machine, current, clock, 500);
    std::cout << "after 500 ms: " << boost::json::serialize(current.value) << '\n';
    if (current.value != boost::json::value("green")) {
        return 1;
    }

    advance(*machine, current, clock, 510);
    std::cout << "after 1010 ms: " << boost::json::serialize(current.value) << '\n';
    if (current.value != boost::json::value("yellow")) {
        return 1;
    }
    return 0;
}

It prints what the actor’s test prints:

after 500 ms: "green"
after 1010 ms: "yellow"

pop_due releases the due events one at a time, by deadline and then in the order they were scheduled, so an event that a delivered one schedules is ordered as XState orders it.

Testing actors

An actor runs a machine over time: it starts, receives events one at a time, keeps its snapshot, and works with its children. A test of an actor creates it from its machine, starts it, sends it events and checks its snapshot after each. In xstate an actor_system is XState’s createActor and its system in one: create_actor makes the actor, start and send deliver to it, and snapshot_of reads its snapshot. Each call that delivers runs the system until nothing is left to do before it returns, as XState’s send() processes at once, so the check that follows it sees all the event did. The call returns a result<run_outcome>: settled; out_of_fuel when an actor ran out of the fuel its budget gives (Every fuel budget); or, when the call is refused, xactor’s errc::invalid_argument, of the category webcpp.xactor, which changes nothing. A call is refused for an actor the system does not know, for an answer to a request no longer pending (Promise actors), and when it is made from inside a callback the test registered on the system, such as a mock (Actions). A test with plenty of fuel expects settled.

Stately’s second example tests a toggle that counts its activations. Its assign, a function in XState’s config, is count_activation here, registered as countActivation: it reads the context it is handed and fails when count is not an integer there (Updating context with assign). expect gathers the test’s checks of the snapshot, Stately’s expect calls, in one function that also prints what it checks:

/** Counts one more activation in the context. */
xstate::result<boost::json::object> count_activation(const xstate::action_args& args) {
    const boost::json::value* count =
        args.context.is_object() ? args.context.get_object().if_contains("count") : nullptr;
    if (count == nullptr || !count->is_int64()) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"count", count->get_int64() + 1}};
}

/** Whether the actor's snapshot has `value` and `context`; prints both. */
bool expect(const xstate::actor_system& system, xstate::actor_ref actor, std::string_view value,
            std::string_view context) {
    const xstate::snapshot* now = system.snapshot_of(actor);
    if (now == nullptr) {
        return false;
    }
    std::cout << boost::json::serialize(now->value) << ' ' << boost::json::serialize(now->context)
              << '\n';
    return now->value == boost::json::parse(value) && now->context == boost::json::parse(context);
}

int main() {
    xstate::implementations implementations;
    implementations.actions.emplace("countActivation",
                                    xstate::assign_action{.assignment = count_activation});
    const boost::json::value config = boost::json::parse(R"({
        "initial": "inactive",
        "context": {"count": 0},
        "states": {
            "inactive": {
                "on": {"activate": {"target": "active", "actions": "countActivation"}}
            },
            "active": {"on": {"deactivate": "inactive"}}
        }
    })");
    const xstate::result<xstate::machine> toggle_machine =
        xstate::create_machine(config, implementations);
    if (!toggle_machine.has_value()) {
        return 1;
    }

    xstate::actor_system system({.fuel = 10'000});
    const xstate::result<xstate::actor_ref> actor = system.create_actor(*toggle_machine);
    if (!actor.has_value() || system.start(*actor) != xstate::run_outcome::settled) {
        return 1;
    }

    // The initial state
    if (!expect(system, *actor, R"("inactive")", R"({"count": 0})")) {
        return 1;
    }

    // Send an event and test the transition
    const xstate::event activate{.type = "activate", .payload = {}};
    if (system.send(*actor, activate) != xstate::run_outcome::settled ||
        !expect(system, *actor, R"("active")", R"({"count": 1})")) {
        return 1;
    }

    // Send another event
    const xstate::event deactivate{.type = "deactivate", .payload = {}};
    if (system.send(*actor, deactivate) != xstate::run_outcome::settled ||
        !expect(system, *actor, R"("inactive")", R"({"count": 1})")) {
        return 1;
    }
    return 0;
}

It prints:

"inactive" {"count":0}
"active" {"count":1}
"inactive" {"count":1}

Each send returned settled once the system had done everything the event caused, so each expect after it saw the transition and the assign.

Mocking effects

An effect is what a machine asks of the world: send an email, write a log, fetch data. A test of the logic replaces each effect with a stand-in that records what was asked and answers as the test decides, so that the test is fast, isolated and the same on every run.

Actions

xstate’s actor system hands every custom action to the function on_action registers, with the actor that returned it, as the actor resolves it (Watching an actor). That function is the mock: it records each call, and the test checks the records. Stately’s test makes its mock with Vitest’s vi.fn() and checks it with toHaveBeenCalledWith; here the mock is a lambda that keeps the type and the params of each action it is handed:

int main() {
    const boost::json::value config = boost::json::parse(R"({
        "initial": "idle",
        "states": {
            "idle": {
                "on": {
                    "start": {
                        "target": "running",
                        "actions": {"type": "logMessage", "params": {"message": "Started!"}}
                    }
                }
            },
            "running": {}
        }
    })");
    const xstate::result<xstate::machine> machine = xstate::create_machine(config, {});
    if (!machine.has_value()) {
        return 1;
    }

    xstate::actor_system system({.fuel = 10'000});
    std::vector<std::string> calls;
    const xstate::result<void> acting =
        system.on_action([&calls](xstate::actor_ref, const xstate::action& returned) {
            calls.push_back(returned.type + ' ' + boost::json::serialize(returned.params));
        });
    if (!acting.has_value()) {
        return 1;
    }
    const xstate::result<xstate::actor_ref> actor = system.create_actor(*machine);
    if (!actor.has_value() || system.start(*actor) != xstate::run_outcome::settled) {
        return 1;
    }

    const xstate::event start{.type = "start", .payload = {}};
    if (system.send(*actor, start) != xstate::run_outcome::settled) {
        return 1;
    }

    const xstate::snapshot* now = system.snapshot_of(*actor);
    const std::vector<std::string> expected{R"(logMessage {"message":"Started!"})"};
    if (now == nullptr || now->value != boost::json::value("running") || calls != expected) {
        return 1;
    }
    std::cout << "value: " << boost::json::serialize(now->value) << '\n';
    for (const std::string& call : calls) {
        std::cout << call << '\n';
    }
    return 0;
}

It prints:

value: "running"
logMessage {"message":"Started!"}

A built-in log never reaches on_action: it reaches the function on_log registers, with its value and its label, and an event the machine emits reaches the listeners on_emitted registers. A test records them the same way (Watching an actor).

A mock only records. It runs inside an actor’s turn, while the system runs: it may read the system, with snapshot_of or status_of, but any call from it that changes the system, such as a send, a resolve or a registration, is refused with invalid_argument and changes nothing. A test whose mock must answer reads the records once the call that ran the system has returned, and answers then. XState accepts a send from an action, through its self, or from a subscriber, and handles it after the update in progress (A callback that calls the system back).

Promise actors

XState runs a promise in a fromPromise actor; a test replaces the promise with a mock and waits a tick for it to settle. xstate has no promise. A host_actor stands for one: its work is done by the host, outside the machines (Host actors). Once started, it is a pending host_request in host_requests(), with its src and its input, and the host answers it with resolve(request, output) or reject(request, error). In a test the test is the host: it checks what was asked once the call that started the request has returned, answers when it chooses with what it chooses, and has nothing to wait for. Here the machine invokes fetchData, a host actor, while it is loading, and the test answers with the data:

int main() {
    xstate::implementations implementations;
    implementations.actors.emplace("fetchData", xstate::host_actor{});
    const boost::json::value config = boost::json::parse(R"({
        "initial": "idle",
        "states": {
            "idle": {"on": {"fetch": "loading"}},
            "loading": {
                "invoke": {"src": "fetchData", "onDone": "success", "onError": "error"}
            },
            "success": {},
            "error": {}
        }
    })");
    const xstate::result<xstate::machine> machine = xstate::create_machine(config, implementations);
    if (!machine.has_value()) {
        return 1;
    }

    xstate::actor_system system({.fuel = 10'000});
    const xstate::result<xstate::actor_ref> actor = system.create_actor(*machine);
    if (!actor.has_value() || system.start(*actor) != xstate::run_outcome::settled) {
        return 1;
    }

    const xstate::event fetch{.type = "fetch", .payload = {}};
    if (system.send(*actor, fetch) != xstate::run_outcome::settled) {
        return 1;
    }

    // The machine waits for the host, which the test plays.
    const std::vector<xstate::host_request> requests = system.host_requests();
    if (requests.size() != 1 || requests.front().src != "fetchData") {
        return 1;
    }
    std::cout << "requests: " << requests.front().src << '\n';
    const boost::json::value output = boost::json::parse(R"({"data": "test"})");
    if (system.resolve(requests.front(), output) != xstate::run_outcome::settled) {
        return 1;
    }

    const xstate::snapshot* now = system.snapshot_of(*actor);
    if (now == nullptr || now->value != boost::json::value("success")) {
        return 1;
    }
    std::cout << "value: " << boost::json::serialize(now->value) << '\n';
    return 0;
}

It prints:

requests: fetchData
value: "success"

reject fails the request instead, and the invoke’s onError takes the error. A request whose actor has ended, because the state that invoked it was left, is no longer pending: answering it is refused with invalid_argument and changes nothing, as XState ignores a promise that settles too late.

Every fuel budget

An actor system runs on fuel (Fuel, parking and resume). Each host call opens executions with the fuel its budget gives, actor_system({.fuel = n}), and every microstep, every message an actor sends and every timer it arms costs one unit. When an execution runs out, its actor parks with what it has not done, the call returns out_of_fuel, and resume() gives every parked actor a new execution. While an actor is parked, the host’s own calls, start, send, clock_tick, resolve and reject, are held in the order made and return out_of_fuel, and resume() applies them once the parked actors have settled. So a run that is resumed until it settles must do what a run with plenty of fuel does, whatever the budget: the same macrosteps, in the same order for each actor, to the same end. That is what this test checks. XState has no fuel, so this test has no twin (The XState twins); xstate’s own actor test, actors_time, runs the same test on more scripts (The tests).

The family: app invokes a worker, a machine_actor (Invoking actors). On go the app sends ping to the worker, a send_to_action, and the worker counts it and answers its parent with pong, a send_parent_action, which the app counts (Sending events between actors); after 1000 ms the app finishes, which stops the worker.

xstate::implementations worker_implementations;
worker_implementations.actions.emplace("countPing", counting("pings"));
worker_implementations.actions.emplace("pong", xstate::send_parent_action{
                                                   .event = making("pong"),
                                                   .id = std::nullopt,
                                                   .delay = std::nullopt,
                                               });
const boost::json::value worker_config = boost::json::parse(R"({
    "id": "worker",
    "context": {"pings": 0},
    "on": {"ping": {"actions": ["countPing", "pong"]}}
})");
const xstate::result<xstate::machine> worker =
    xstate::create_machine(worker_config, worker_implementations);
if (!worker.has_value()) {
    return 1;
}

xstate::implementations app_implementations;
app_implementations.actors.emplace("worker", xstate::machine_actor{*worker});
app_implementations.actions.emplace("countPong", counting("pongs"));
app_implementations.actions.emplace("ping", xstate::send_to_action{
                                                .target = "worker",
                                                .event = making("ping"),
                                                .id = std::nullopt,
                                                .delay = std::nullopt,
                                            });
const boost::json::value app_config = boost::json::parse(R"({
    "id": "app",
    "initial": "working",
    "context": {"pongs": 0},
    "states": {
        "working": {
            "invoke": {"src": "worker", "id": "worker"},
            "on": {"go": {"actions": "ping"}, "pong": {"actions": "countPong"}},
            "after": {"1000": "finished"}
        },
        "finished": {"type": "final"}
    }
})");
const xstate::result<xstate::machine> app =
    xstate::create_machine(app_config, app_implementations);
if (!app.has_value()) {
    return 1;
}

run_with runs a script of host calls on a system with a given budget, resuming after each call until the system settles, and records what each actor did, by its id: every macrostep it settled, which the inspector (inspect) reports with the event that began it (Inspection), and its status at the end. clock_tick(1000) moves the system’s clock to 1000 ms, the time to move to and not an increment (Time), and the app’s timer, due then, fires.

/** What each actor did in a run, by its id: every macrostep it settled, then its status. */
using trace = std::map<std::string, std::vector<std::string>, std::less<>>;

/** A run: its trace, and how many times the host had to resume it. */
struct run {
    trace actors;
    std::size_t resumes = 0;
};

/**
 Resumes a system until no actor is parked; how many resumes it took, or
 nothing when a call failed or it never settled.
*/
std::optional<std::size_t> settle(xstate::actor_system& system,
                                  xstate::result<xstate::run_outcome> outcome) {
    constexpr std::size_t most = 100'000;
    std::size_t resumes = 0;
    while (outcome.has_value() && *outcome == xstate::run_outcome::out_of_fuel && resumes < most) {
        outcome = system.resume();
        ++resumes;
    }
    if (!outcome.has_value() || *outcome != xstate::run_outcome::settled) {
        return std::nullopt;
    }
    return resumes;
}

/** Runs the test's script on a system whose executions have `fuel` each. */
std::optional<run> run_with(const xstate::machine& app, std::uint32_t fuel) {
    xstate::actor_system system({.fuel = fuel});
    run made;
    std::vector<xstate::actor_ref> started;
    const xstate::result<void> inspecting = system.inspect(xstate::inspector{
        .started = [&started](xstate::actor_ref actor) { started.push_back(actor); },
        .settled =
            [&system, &made](xstate::actor_ref actor, const xstate::machine& /*logic*/,
                             const xstate::event& cause, const xstate::snapshot& settled) {
                made.actors[std::string(system.id_of(actor))].push_back(
                    cause.type + " -> " + boost::json::serialize(settled.value) + ' ' +
                    boost::json::serialize(settled.context));
            },
        .emitted = {},
    });
    if (!inspecting.has_value()) {
        return std::nullopt;
    }
    const xstate::result<xstate::actor_ref> root =
        system.create_actor(app, {.input = nullptr, .id = "app", .system_id = std::nullopt});
    if (!root.has_value()) {
        return std::nullopt;
    }

    const xstate::event go{.type = "go", .payload = {}};
    const std::vector<std::function<xstate::result<xstate::run_outcome>()>> script{
        [&] { return system.start(*root); },
        [&] { return system.send(*root, go); },
        [&] { return system.send(*root, go); },
        [&] { return system.clock_tick(1000); },
    };
    for (const auto& call : script) {
        const std::optional<std::size_t> resumes = settle(system, call());
        if (!resumes.has_value()) {
            return std::nullopt;
        }
        made.resumes += *resumes;
    }
    for (const xstate::actor_ref actor : started) {
        const xstate::result<xactor::status> status = system.status_of(actor);
        if (!status.has_value()) {
            return std::nullopt;
        }
        made.actors[std::string(system.id_of(actor))].push_back("status " +
                                                                std::string(name_of(*status)));
    }
    return made;
}

The test runs the script with plenty of fuel, and then with every budget from 1 to 40, and compares the records:

const std::optional<run> plenty = run_with(*app, 10'000);
if (!plenty.has_value() || plenty->resumes != 0) {
    return 1;
}
for (const auto& [id, entries] : plenty->actors) {
    for (const std::string& entry : entries) {
        std::cout << id << ": " << entry << '\n';
    }
}

for (std::uint32_t fuel = 1; fuel <= 40; ++fuel) {
    const std::optional<run> scarce = run_with(*app, fuel);
    if (!scarce.has_value() || scarce->actors != plenty->actors) {
        std::cout << "fuel " << fuel << " differs\n";
        return 1;
    }
    if (fuel == 1) {
        std::cout << "fuel 1: the same, after " << scarce->resumes << " resumes\n";
    }
}
std::cout << "every budget from 1 to 40: the same\n";

It prints:

app: xstate.init -> "working" {"pongs":0}
app: go -> "working" {"pongs":0}
app: pong -> "working" {"pongs":1}
app: go -> "working" {"pongs":1}
app: pong -> "working" {"pongs":2}
app: xstate.after.1000.app.working -> "finished" {"pongs":2}
app: status done
worker: xstate.init -> {} {"pings":0}
worker: ping -> {} {"pings":1}
worker: ping -> {} {"pings":2}
worker: status stopped
fuel 1: the same, after 14 resumes
every budget from 1 to 40: the same

With one unit of fuel per execution the host had to resume the system 14 times, and each actor did what it did with plenty. The records are compared actor by actor, not as one sequence. xactor queues every delivery: each actor has a first-in first-out mailbox, the actors that have work wait in a first-in first-out queue, and a turn hands one message to its actor. XState delivers an event to an idle actor at once, nested in its sender’s call, and queues it for a busy one. So the order between two actors may differ, while the order within each is XState’s (Interleaving across actors).

Model-based testing

A test written by hand checks the paths its author thought of. Model-based testing takes the paths from the machine: every state it can reach, and a shortest sequence of events that reaches it, each path a test to run against the system the machine models. Stately’s page closes with XState’s tools for it, xstate/graph, formerly @xstate/test.

xstate has no port of xstate/graph. A snapshot is a value and transition a pure function, so a test walks a machine itself: get_next_transitions lists the transitions a snapshot could take (Every transition a snapshot could take), as XState’s graph takes its events from the snapshot’s own event descriptors. The list holds every transition, so a type may come more than once, from several guarded transitions of a state or from a state and its ancestor, where XState’s descriptors are a set. shortest_paths walks breadth first. From each snapshot it sends each event type offered once, with get_next_snapshot, and keeps each snapshot it has not seen, known by its value and its context, as XState’s graph keys a snapshot. An eventless transition is no event to send: the macrostep takes it on its own. The machine runs, pauses, resumes and stops:

/** A snapshot a machine can reach, and the events of a path that reaches it. */
struct path {
    xstate::snapshot reached;
    std::vector<std::string> events;
};

/** A snapshot's identity in the walk: its value and its context, as XState's graph keys it. */
std::string key_of(const xstate::snapshot& reached) {
    return boost::json::serialize(reached.value) + boost::json::serialize(reached.context);
}

/**
 The shortest path to every snapshot a machine can reach, breadth first: from
 each snapshot, each event type get_next_transitions offers is sent once.
*/
std::vector<path> shortest_paths(const xstate::machine& machine) {
    std::vector<path> found;
    found.push_back(path{
        .reached = xstate::get_initial_snapshot(machine),
        .events = {std::string(xstate::init_event_type)},
    });
    std::set<std::string, std::less<>> seen{key_of(found.front().reached)};
    for (std::size_t next = 0; next < found.size(); ++next) {
        // A copy: what found holds may move when a path is added below.
        const path from = found[next];
        // Each type once, as XState's descriptors are a set: several transitions may offer it.
        std::set<std::string_view, std::less<>> types;
        for (const xstate::transition_definition* offered :
             xstate::get_next_transitions(machine, from.reached)) {
            if (offered->event_type.empty() || !types.insert(offered->event_type).second) {
                continue;
            }
            const xstate::event sent{.type = offered->event_type, .payload = {}};
            xstate::snapshot reached = xstate::get_next_snapshot(machine, from.reached, sent);
            if (!seen.insert(key_of(reached)).second) {
                continue;
            }
            std::vector<std::string> events = from.events;
            events.push_back(offered->event_type);
            found.push_back(path{.reached = std::move(reached), .events = std::move(events)});
        }
    }
    return found;
}

int main() {
    const boost::json::value config = boost::json::parse(R"({
        "initial": "idle",
        "states": {
            "idle": {"on": {"start": "running"}},
            "running": {"on": {"pause": "paused", "stop": "idle"}},
            "paused": {"on": {"resume": "running", "stop": "idle"}}
        }
    })");
    const xstate::result<xstate::machine> machine = xstate::create_machine(config, {});
    if (!machine.has_value()) {
        return 1;
    }

    const std::vector<path> paths = shortest_paths(*machine);
    if (paths.size() != 3) {
        return 1;
    }
    for (const path& one : paths) {
        std::cout << boost::json::serialize(one.reached.value) << ": [";
        for (std::size_t index = 0; index < one.events.size(); ++index) {
            std::cout << (index == 0 ? "" : ", ") << one.events[index];
        }
        std::cout << "]\n";
    }
    return 0;
}

It prints:

"idle": [xstate.init]
"running": [xstate.init, start]
"paused": [xstate.init, start, pause]

Each path is a test: start the machine, send the path’s events after its first, then check the state the system reached. The first entry, xstate.init, stands for the start, as in XState’s paths, and is not sent: from outside, XState’s macrostep selects no transition for it, where xstate’s selects transitions for it as for any other event (Events). The walk sends each event with an empty payload. A machine whose guards read the payload needs the events chosen for it, as XState’s getShortestPaths takes them in its events option.

When two events reach a snapshot in as many steps, the walk keeps the path of the event it tries first, in the order get_next_transitions lists them. Within one state that is the order in which XState formats the state’s transitions: its on events, then its onDone, invoke and after events, wherever the config writes those. Within on and within after, the keys come in the order JavaScript enumerates an object’s keys: a key that is an array index, such as 1 or 100, first, by number, then the others as the config writes them. Across states, an active atomic state’s transitions come before those of its ancestors, where XState’s graph takes the event descriptors of the active states root first. So in a machine whose initial state a goes to its sibling b on go and on stop, and whose root also takes stop to b, the walk reaches b by go, the first event a lists, and XState’s graph by stop, which the root lists.

How xstate is tested

xstate is a port, and a port is right when it does what the original does. So the tests that check behaviour XState also has do not state by hand what XState does, which could carry a translation error that nothing catches: they ask XState itself. XState runs every case, each ported from its own tests or written where they are silent, and what it records is what the suites hold xstate to (XState as the oracle, The ported cases); and XState runs a twin of every example of this page that it can run (The XState twins). The tests written by hand cover the rest: what XState has no test for, what the oracle cannot see, and a few of XState’s expectations, copied where no case can hold them (The tests). Every program of this page is a test too, built and run on every target (The examples).

Each is a target of b2, run from the root of a webcpp checkout:

b2 libs/xstate/test

The 39 tests, natively: the lightweight_test programs, the 2 Boost.Test suites that run the ported cases, each of the 24 public headers compiled alone, and core_alone and user_aggregates, which must compile (The tests). It needs a C++20 compiler and Boost’s headers, and no Node.

b2 libs/xstate/example

The 146 examples, each built, run, and its output compared with the one committed beside it (The examples).

b2 libs/xstate/test libs/xstate/example toolset=clang-wasip2 testing.launcher=wasmtime

The same for wasm32-wasip2: 37 tests, the Boost.Test suites being native only, and the 146 examples, each run by wasmtime (Targets). toolset=clang-wasip3 does the same for wasm32-wasip3: 37 tests and 146 examples.

b2 libs/xstate/test/oracle//oracle

The oracle lane: XState 5.33.2 runs every case, and what it records must be what the suites hold (XState as the oracle); and every twin runs on XState and must print what its program printed (The XState twins). It needs Node and npm. The continuous integration runs it from scratch, beside the lanes, in its job Own lane (xstate, oracle).

b2 libs/xstate/test/oracle//update-expected

Not a check: it writes XState’s output again, over the committed one (Regenerating XState’s output).

b2 compares timestamps, so a result that counts is built from scratch, with b2 -a. b2 libs/xstate/test//machine builds and runs one test alone, while working on it.

Targets

test/Jamfile and example/Jamfile declare the targets they are built for, webcpp.targets native wasip2 wasip3, and b2 builds each program for those alone:

Target How it is built and run

native

Any toolset not listed below. As the build makes it: with exceptions, unless its user asks for exception-handling=off, which links webcpp’s tools/throw_exception.cpp. The 2 Boost.Test suites are built for this target alone.

wasip2

The toolset clang-wasip2, the Clang of wasi-sdk for wasm32-wasip2, run by wasmtime. With exceptions, as WebAssembly’s exception handling, as on wasip3.

wasip3

The toolset clang-wasip3, for wasm32-wasip3, run by wasmtime. With exceptions, as WebAssembly’s exception handling.

That xstate works without exceptions is checked by the lint, which compiles every public header, and every test and example, which instantiate its templates, with exception-handling=off; a template that none of them instantiates is not checked.

<webcpp/xstate/config.hpp>, which every public header of xstate includes first, defines WEBCPP_XSTATE_NO_EXCEPTIONS when webcpp has no exceptions (WEBCPP_HAS_EXCEPTIONS is not defined: the compiler has none, or the program defined WEBCPP_NO_EXCEPTIONS), and a program may define it for xstate alone, to turn xstate’s exceptions off in a build that has them, as Boost.Asio’s BOOST_ASIO_NO_EXCEPTIONS does. xstate raises no exception of its own (where XState throws, it returns an error), so the macro changes nothing in it: it exists so that a program can set it for every webcpp library alike. The tests config, config_developer_defined and config_all_off check the three ways it is defined, and the lint refuses a throw in a public header, even in a template nothing instantiates: an exception goes through boost::throw_exception.

The WASI toolsets are b2’s clang, which a user-config.jam registers against wasi-sdk, as xactor’s page shows. The Boost.Test suites read the fixture files at run time, which a wasm program cannot without a directory its runtime opens for it, so they run natively; every other test, and every example, runs on native, wasip2 and wasip3.

The continuous integration runs xstate’s tests and examples in lanes, one toolset each: GCC 14 and 15, Clang 18 and 22, Apple Clang, MSVC 14.3 and 14.5, and wasip2 and wasip3. On Linux, GCC and Clang build with libstdc++, the system’s standard library there, so the lanes clang-linux-18 and clang-linux-22 run xstate with Clang on libstdc++. Each lane builds from scratch and records what b2 built and ran:

b2 -a --dump-tests --out-xml=<lane>.xml toolset=<toolset> libs/xstate/test libs/xstate/example

webcpp’s tools/report/report.py merges the files of every lane into the test matrix, published at https://webcpporg.github.io/report/, where each cell says whether a program passed in a lane, or how it failed. Beside the lanes, tools/lint/lint.sh runs clang-format and clang-tidy from wasi-sdk, with rules of webcpp’s own: the licence notice every source file opens with, the layout of JSON literals, the machine core’s boundary, which names the file and the line of any include of xactor or of the actor layer from the core, and the patterns webcpp bans, among them any real clock, file, socket or process a header could name, which no test can show xstate never reaches (guarantee 15).

XState as the oracle

test/oracle/ pins the npm package xstate at 5.33.2, exactly, with its lockfile, from which the first target that needs it installs it with npm ci; its Jamfile declares the lane with webcpp’s webcpp.cases and webcpp.twins. Its scripts run xstate’s test cases on it and write what XState produced:

  • oracle.mjs runs the machine cases. Each macrostep after the initial one runs through XState’s machine.transition, with an actor scope of the script’s own that records every action and every @xstate.microstep inspection event, and whose actor has a parent, so that a sendParent resolves as it does in an actor that has one. XState’s pure functions cannot serve: their actor has no parent, and throws on sendParent. The initial macrostep runs through machine.getInitialSnapshot, with the same scope; XState’s inspection does not report its first microstep, so the snapshots and actions of its microsteps are taken from getInitialMicrosteps. Delayed events go to XState’s own SimulatedClock.

  • actors.mjs runs the actor cases on XState’s actors, where a host actor is a promise the script settles when a step says so. After each step it writes, for every actor, the events it processed with the snapshot each left and the events it emitted, then every actor’s status and the host’s pending requests. An actor is keyed by its path of ids from the root, and, when an earlier actor had the same path, by its rank among them (root/n#2); the requests come in the order of the keys of their actors. No nesting of deliveries changes those keys. It never writes the order between two actors, which xactor’s queues may change (Interleaving across actors).

Both run XState’s development build, which XState’s own tests run. It makes checks the production build leaves out, and three of them are checks a JSON machine can meet: an event of the type * sent from outside, a transition that declares a truthy cond, a forwardTo whose target is no actor. xstate makes the same three, so its oracle must (The checks of XState’s development build): development.mjs refuses any other build, and every command runs node with --conditions=development. No library, suite or example uses the package, and its output is committed, so the suites run without node.

The ported cases

XState’s tests are TypeScript with inline functions. Each test that a machine written in JSON can express is ported to a case: under test/fixtures/cases/ for the machine core, and test/fixtures/actors/cases/ for the actor layer. A file holds the cases ported from one XState test file, or from a range of lines of a long one (invoke_1.json to invoke_5.json), under source, cases and excluded. A case names the test it ports, holds the machine’s config as JSON, and lists its steps. The first case of after.json, ported from after.test.ts, is the test of Time:

{
    "name": "transition after delay through yellow",
    "test": "after.test.ts > delayed transitions > should transition after delay",
    "machine": {
        "id": "light",
        "initial": "green",
        "context": {"canTurnGreen": true},
        "states": {
            "green": {"after": {"1000": "yellow"}},
            "yellow": {"after": {"1000": [{"target": "red"}]}},
            "red": {"after": {"1000": "green"}}
        }
    },
    "steps": [
        {"start": true},
        {"query": {"matches": "green"}},
        {"advance": 500},
        {"query": {"matches": "green"}},
        {"advance": 510},
        {"query": {"matches": "yellow"}}
    ]
}

A step of a machine case starts the machine (start, with an optional input), sends it an event (send), moves the clock (advance), resolves a state value (resolve) or asks a question of the snapshot (query: matches, has_tag, can, meta, next_transitions). A case may also expect the machine to be refused: expect_create_error when XState refuses it too, xstate_refuses with the error code when xstate refuses at creation what XState throws on only once it runs (A config is refused when it is created).

An actor case lists the actors its machine may run, machines, each with implementations of its own, and host actors. Its steps create or start the root (create, start, with an optional input and system_id), send to it or to a child (send, with to), move the clock (advance), answer a host request (resolve, reject), find an actor by its systemId (get) and stop the root (stop).

The implementations a case names come from a vocabulary written twice, entry for entry, test/oracle/vocabulary.mjs for XState and test/vocabulary.hpp for xstate: fixed assigns and guards (increment, push, context_at_least and others), the built-in actions a case declares with their options under actions, and any other name, which is a custom action.

A test that cannot be ported is not dropped in silence: each file lists the tests it leaves out, with the reason.

{
    "test": "after.test.ts > delayed transitions > should execute an after transition after starting from a persisted state",
    "reason": "persistence/rehydration: restores an actor from a snapshot taken via getSnapshot/getPersistedSnapshot"
}

The machine cases are 504 cases of 1,111 steps, 413 of them ported from XState’s tests, and leave 159 tests out: 36 of those are ported with the actor cases, and the others need a function JSON cannot hold, TypeScript’s type checker, enqueueActions, an actor reference held as data, callbacks or observables, persistence, machine.provide or XState’s options.maxIterations, which xstate does not read (tests of transient.test.ts, Bounding a macrostep); assert on spies, warnings or XState’s internal objects; or are skipped in XState’s own suite. XState’s SCXML suite is left out whole, because the published package exports no SCXML converter. The actor cases are 228 cases of 501 steps, 163 of them ported from XState’s actor tests (actor, interpreter, invoke, spawnChild, spawn, system, errors, emit, predictableExec, and the actor tests of input, after and the core’s files), and leave 153 tests out: callbacks, observables and transition functions, persistence, actor references as data, functions JSON cannot hold, and the places where the actor layer differs from XState.

Other files are xstate’s own, still checked against XState. The two review.json files, 43 machine cases and 30 actor cases, pin what XState does where its own tests are silent: among them a machine whose root is a history node, an event of the type *, a transition that declares cond, a value computed as undefined, a timer armed twice under one id, a history that is neither shallow nor deep, the order in which JavaScript enumerates the keys of delays, regions, events and state values, and, among actors, the systemId of a child born done and the snapshot a deferred failure reached. resolution.json, 12 actor cases, pins how XState resolves spawnChild, stopChild and sendTo in an actor; vocabulary.json, 16 actor cases, pins the vocabulary of the cases against XState; actor_actions.json, computed.json and invoke_pure.json, 47 machine cases, run the actions that name actors, the computed values and invokes through the pure functions; and each oracle has a smoke.json of its own.

The XState twins

The cases hold the library to XState; the twins hold this page to it. This page shows C++ only, and the comparison with XState that a reader no longer sees stays a test. Every program of example/xstate/ and example/actors/, the programs of this page, has a twin in test/oracle/twins/, under the same group and name: <name>.mjs, a Node program that does on XState 5.33.2 what <name>.cpp does on xstate, and prints what it finds the same way. Of the twins of this chapter’s programs, testing_<name>.mjs, 9 are tests of Node’s own runner, node:test, and assert with node:assert, where Stately’s page uses Vitest, so that each runs on Node alone.

A program is of one of three kinds:

  • Its twin agrees, and prints the program’s output, the one this page shows under the program. 114 twins agree.

  • Its twin diverges, on purpose. The program shows a place where xstate differs from XState, and the twin prints what XState does there. That output is committed beside the twin, as <name>.expected, and must differ from the program’s: an output equal to it is a fault, since that twin agrees. 22 twins diverge, and Where xstate differs from XState shows each output beside its program’s, with the reason for the difference.

  • It has no twin, because it shows what XState has no counterpart for: the macrostep cursor, fuel, a registry of inputs and outputs by invoke id or state id, a machine created again from its definition, an event read from JSON. 10 programs have none, and each has a line <group>/<name>: <reason> in test/oracle/twins/without-twin.txt.

So the 146 programs, 89 of example/xstate/ and 57 of example/actors/, are 114 that agree, 22 that diverge and 10 without a twin.

webcpp’s tools/oracle/twins.py runs the twins and compares, declared in test/oracle/Jamfile with webcpp.twins. It runs the twins in parallel, each in a process of its own:

  • on XState’s development build, node --conditions=development, which the oracle runs too (XState as the oracle);

  • with the report of Node’s test runner sent to the standard error (--test-reporter=spec --test-reporter-destination=stderr), so that only what the twin prints reaches the standard output;

  • with colour off, NO_COLOR set and FORCE_COLOR removed, whatever the shell asks for;

  • for at most 60 seconds, after which the twin and every process it started are killed, and the twin is reported.

What the twin prints on the standard output, every carriage return removed, must equal the program’s <name>.expected, or, for a twin that diverges, the twin’s own. A difference fails the target, and twins.py prints the diff; so does a twin that exits with an error or does not end in time. So does a mismatch of the directories: a program with neither a twin nor a line in without-twin.txt, a program with both, a twin or a line that names no program, an output of no twin, or a line that is not <group>/<name>: <reason>. A node that is not on the PATH, a directory that is not there, or a directory of examples with no program stops it at once, named. The twins run alone, in a few seconds:

b2 libs/xstate/test/oracle//twins

The output of a twin that diverges is never written by hand. A new one starts as an empty file beside the twin, and b2 libs/xstate/test/oracle//update-expected fills it with what XState prints (Regenerating XState’s output). That update rewrites only the outputs that exist, and refuses to write one equal to its program’s output: it reports that twin, which agrees, and leaves its output file as it was, for its author to delete.

The tests

xstate’s tests are under test/, each declared in test/Jamfile with one of webcpp’s rules. Most are programs of Boost.Core’s lightweight_test, declared with webcpp.run and run on every target. Each test case is a function, main calls them all and returns boost::report_errors(), and a case that cannot go on after a failed check returns; a helper, which cannot return from its case, calls webcpp::test::require, of test/require.hpp, which ends the program with the errors counted so far. So a test needs no exceptions to stop, and runs as it is in a user’s build without them.

Test What it checks

values

The error codes, state values, paths and events. Its matchesState rows are the expectations of XState’s match.test.ts, copied one row per expect().

machine

Creation, with XState’s ids, types and document order stated by hand; lookup; and every refusal, an implementation of each kind holding an empty function among them.

cursor

The cursor, which XState does not have: the microstep that settles and no other, an event that selects no transition, a copy that resumes, a cycle that never settles, and a failure that settles in error, reporting what it resolved before the failure apart from its actions. And rules of the machine core, stated by hand: a spawn whose input fails settles in error without the child; a sendTo to a systemId is returned for its caller to resolve; a sendTo to an invoke of the state being entered is bound at the end of that state’s entry actions, spawns and initial actions, the point bound_at counts to (action::bound_at); and the clock schedules only a built-in delayed raise (Delayed events and the simulated clock).

actors_lifecycle

What the oracle cannot see of an actor’s life: created, started, sent events, stopped, done or failed, what a failing macrostep hands over, the ids the system names its actors with, and the host’s stop of a child, which XState refuses too.

actors_sending

The events actors send each other and the system that delivers them: a sendTo, a sendParent and a send to the actor itself, an error sent as an event, and the systemIds that name actors while they run, with their refusals.

actors_children

The children actors invoke and spawn: a child’s output and error reaching its parent, its stop when its state is left, the order the children of a start start in, and the children of a macrostep that fails.

actors_time

Time and fuel. The timers due during a tick, delayed events and cancels, and the fuel each microstep costs; and parking: every budget from 1 to 40 resumed to the end doing what plenty of fuel does, for a child’s start, a deferred send or emit, a done or an error report, a host’s answer, a timer and a stop that reaches a grandchild; the host’s calls held behind a parked actor; a child done while its parent is parked.

actors_host

What the host sees and answers: host actors, their requests resolved and rejected, a late answer to a request, and the callbacks that hand over custom actions, logs, snapshots and emitted events, which cannot call the system back.

The ported cases run in 2 Boost.Test suites, cases and actors_cases, declared with webcpp.boost-test, which read the fixture files and register one test case for each case, so that a failure names its case. A wasm program cannot read them without a directory its runtime opens for it, so these suites run natively only:

Suite What it checks

cases

Every machine case, one test case each, against XState’s record.

actors_cases

Every actor case, one test case each, against XState’s record, actor by actor.

cases runs a case as a caller that pays fuel runs a machine: each macrostep through the cursor, one microstep a call, at most 10,000 of them, exactly one of which must say it settled. It writes each microstep as the oracle writes it, the event, the transitions, the actions and the snapshot (value, context, status, output, error, tags sorted, children, and the active nodes in order), and compares every step with XState’s. It also compares the machine’s definition, to_json, with XState’s machine.toJSON(), and where that definition holds the whole machine, it loads the machine back from it and runs the case again. actors_cases runs each actor case on an actor_system and compares, after every step, what each actor did with what XState’s did, under the keys the oracle writes. In both, a case file without XState’s output, or an output file that is not one, fails its cases rather than vanishing.

Beside them, test/Jamfile compiles each of the 24 public headers alone, as the only include of its translation unit, so that a header that leans on an include it does not make fails, naming it. core_alone checks that the machine core includes nothing of xactor or of the actor layer, every include guard of theirs still undefined after <webcpp/xstate.hpp>; and user_aggregates compiles a user’s designated initializers that leave members out, as the user’s own code, with -Wextra and warnings as errors.

While working on one test, build and run it alone; before saying a change is done, run every test from scratch, and then the oracle:

b2 libs/xstate/test//cases
b2 -a libs/xstate/test libs/xstate/example
b2 -a libs/xstate/test/oracle//oracle

The examples

Every program this page shows is a test. b2 libs/xstate/example builds each program of example/xstate/ and example/actors/, declared in example/Jamfile with webcpp.example, runs it, and compares what it printed on the standard output, every carriage return removed, with the <name>.expected committed beside it, through webcpp’s tools/example/run_example.py. A difference fails the build with a diff. So does a program that returns anything but 0, even when it printed the expected lines, since each program returns 1 when a check of its own fails. Every run compares again, whatever b2 holds up to date, so a run never passes on the result of an earlier one.

The same programs build for wasm32-wasip2 and wasm32-wasip3, and run under wasmtime, through which run_example.py runs each (Targets). A program’s output is compared with the same <name>.expected on every target, so it prints the same on every target, each of which builds it with exceptions.

b2 libs/xstate/doc builds this page from the same files, each program’s source included by tag and its output whole, so neither can drift from the library. Any message of Asciidoctor’s fails it, a reference to no section among them, and so does an output whose program is gone, a program the page does not show, or the output of a twin that diverges that the page does not show.

Regenerating XState’s output

XState’s output is committed: the oracle’s under test/fixtures/expected/ and test/fixtures/actors/expected/, and that of each twin that diverges beside the twin (The XState twins). After a case is added or changed, or a twin that diverges, update-expected runs the scripts on every case and replaces the committed output with what they write, then rewrites the output of every twin that diverges with what XState prints. Its diff is what XState does, and so what the suites and this page will hold xstate to; read it, then run the suites. It needs Node.

b2 libs/xstate/test/oracle//update-expected

The oracle lane runs the same scripts into its build directory, compares what they wrote with the committed output, file by file and byte by byte, through webcpp’s tools/oracle/compare.py, and fails when the two differ; it also runs the twins against the outputs committed beside them. A case changed without its output, or an output edited by hand, fails the lane, so the committed expectations are always what XState 5.33.2 produces for the committed cases. The oracle lane is the one place xstate’s tests need Node; b2 libs/xstate/test does not, and never runs npm.

Guarantees

What xstate promises whatever its caller does: the rules its results keep, which the sections above teach and the tests check. A guarantee keeps its number, and the anchor named after it, so a comment of the code or of a test cites it by that anchor: (doc: #xstate-invariant-13) sends its reader to the thirteenth guarantee, and (doc: #xstate-invariant-a3) to the third of the actor layer. b2 libs/xstate/doc, which builds this page, fails on a citation that names no anchor of it (How xstate is tested). What xactor promises, which the actor layer builds on, is on xactor’s page.

The machine core

  1. A machine is created once from its config and never changes. A config’s states, on and after are read in the order JavaScript enumerates an object’s keys, as XState reads them: a key that is an array index, such as "0" or "500", first, by number, then the rest as written. That order is the document order of the states, the order of a state’s transitions and the order of the raises its after schedules. Creating a machine refuses each of these, with an error and no machine; create_machine gives the code of each, and What creation refuses shows them:

    • a config that is not an object, or a member of the wrong type, such as an invoke’s id or systemId that is not a string;

    • a compound state without initial;

    • an initial or a target that names no state, but for a history at the root, which XState accepts and so does xstate (guarantee 23);

    • an on key of "";

    • a transition that declares a truthy cond, renamed guard in XState v5, whose development build refuses it;

    • a guard, or a named delay of an after key, that the implementations do not hold, where a raise’s or a send’s named delay that they do not hold is no delay, as in XState;

    • an after key that starts with a digit, ., - or + and is not a whole number of milliseconds, such as "1.5" or "5abc" (Time in whole milliseconds);

    • a node whose default entry leads back to it, such as a history whose default target is an ancestor whose initial chain reaches the history again;

    • an invoke whose src is not a string (An invoke’s src), and an actor the implementations do not hold (guarantee 22);

    • an entry, exit or actions that is null, which XState’s toArray reads as a list that holds null and throws on when it runs it;

    • a history’s target that is neither a string nor a list of strings, the root’s included: XState reads a null one the same way and throws on it where it needs the default, and takes any other as it is (A config is refused when it is created);

    • a tags that holds anything but strings, null included;

    • implementations of which one holds an empty function (an assigner, an event, a value or an id maker, a guard, a delay or the context), which calling would abort without exceptions, where an optional one left out holds none; and an entry of implementations::inputs or implementations::outputs that names what it may not (guarantee 24).

  2. An action the implementations do not hold is a custom action. It is returned to the caller as {type, params}, in the order XState returns it, and the core does nothing else with it.

  3. assign changes the context, merging the partial update its implementation returns as Object.assign does, and is never returned as an action. raise, log, cancel, sendParent and the actions of guarantee 21 are returned with their resolved params, sendParent as XState’s xstate.sendTo to #_parent.

  4. A raise without a numeric delay goes to the macrostep’s internal queue; one with a delay is only returned, for the caller’s clock.

  5. A state with after raises xstate.after.<delay>.<id>, with that id and delay, on entry, and cancels it on exit, as XState does.

  6. One call of next() runs exactly one microstep. The first runs the external event’s microstep, or, for the initial macrostep, the initial one; each later call runs one iteration of XState’s macrostep loop.

  7. progress::settled is true on the microstep after which XState’s macrostep loop would stop, and on no other: when no eventless transition is enabled and no raised event is queued, or the status is not active. An external event that selects no transition settles on the first next(), with the snapshot unchanged, unless an eventless transition is enabled after it, as a guard that reads the event can make it, or the event is an actor’s error event, which guarantee 20 settles in error. An external event of the type * is refused: the first next() fails the macrostep with invalid_event, as XState’s development build refuses it, while an event of that type the machine raises takes the wildcard transitions.

  8. The core never stops a macrostep: an always cycle never settles, and the caller’s fuel is the only bound. A cursor is a value, and a copy resumes exactly where the original stopped.

  9. Eventless transitions are selected after the first microstep of a macrostep, the initial one or the external event’s, and after every microstep of a raised event, whatever it changed. After a microstep of eventless transitions they are selected again only when it changed the snapshot: the active nodes, the history, the context (an assign ran), the children (a spawn or a stop resolved, guarantee 18) or the status. This is XState’s comparison of snapshots by identity, stated as a rule: its loop selects them again when the microstep returned another snapshot than the one it began from.

  10. The active nodes are an insertion-ordered set: kept nodes keep their place, exited nodes leave it, and entered nodes are appended in document order. A microstep that ends with the same active nodes and history keeps the order it had, as XState returns its snapshot unchanged; any other microstep that leaves the machine done lists them by descending order instead, each after its descendants, as XState sorts its array of active nodes in place when the machine is done. That order decides which eventless transition a parallel region takes first. A state value’s keys are in the order JavaScript enumerates them, as XState’s value is an object: array indices first, by number, then the rest in the order of the nodes.

  11. An implementation’s failure sets the snapshot’s status to error, with its error code, and settles the macrostep. The failing microstep reports no actions, as XState’s pure functions throw, and neither does the macrostep’s result for it; its progress::resolved_before_failure holds the actions it resolved before the failure, which a running actor of XState has run in part (guarantee A9).

  12. Every result equals XState’s for the same machine and events: each microstep’s event, transitions, actions and snapshot (value, context, status, output, tags, active nodes in order), as the oracle records them in test/fixtures/expected/ (XState as the oracle).

  13. simulated_clock releases due events by (deadline, scheduling order), one at a time, and cannot be set back in time.

  14. to_json (The definition) writes XState’s definition of the machine, key for key, plus the higher-order guards XState cannot write. XState’s definition leaves out the context, the eventless transitions and a history state’s default target, and two states may share an id when a key holds a dot. A machine created from a definition, given its context, takes the original’s steps and writes the same definition, its version included, whenever none of those is in the original and no entry or exit names a custom action xstate.raise or xstate.cancel, which a definition cannot tell from the raise and the cancel an after adds. A definition XState would not write, one with a member of another type than XState writes, such as an entry or an exit that is not a list, is invalid_config.

  15. xstate throws no exception of its own: every operation that can fail returns a result, and the library compiles without exceptions, which the lint checks for every public header and for every test and example, which instantiate its templates. It holds no global or static mutable state, and it never reads a clock, a file or a network.

  16. A machine starts with an input, null when none. The event xstate.init carries a non-null input as its member input. When implementations::context is set, the initial context is what it returns for the input, in place of the config’s context; a failure, or a value that is not an object, fails the initial macrostep on its first next(), with the snapshot before it (XState’s _getPreInitialState).

  17. A state’s invoke, one object or a list, names a child actor by its string src, with an optional id, by default <index>.<state id> (XState’s createInvokeId), input and systemId. Its onDone, onError and onSnapshot are transitions on xstate.done.actor.<id>, xstate.error.actor.<id> and xstate.snapshot.<id>. A node’s transitions are kept in XState’s Map order: on, onDone, each invoke’s three, after; a descriptor given twice keeps its first place and its last transitions, except under after, whose transitions append.

  18. Entering a state returns, after its entry actions and before its initial actions, one xstate.spawnChild per invoke, its params {id, src, input?, systemId?}, and adds the child to the snapshot’s children; exiting it returns, after its exit actions, one xstate.stopChild per invoke, its params {id}, and removes the child, with no params when the child is no longer there. Both change the snapshot, as guarantee 9 counts change. The core runs no child: its caller does. children lists (id, src) in the order JavaScript enumerates XState’s children object: ids that are array indices first, by number, then the others in spawning order.

  19. A macrostep whose snapshot is not active when it settles, done or still in the error it began from, returns, after every microstep’s actions, one xstate.stopChild per child left, in macrostep_result::actions and in no microstep; the snapshot keeps its children (XState’s stopChildren, whose snapshot XState drops). A macrostep that guarantee 20 settles, or one that fails, returns none.

  20. An external event whose type starts with xstate.error.actor and that selects no transition settles in one microstep, with an empty list of transitions and no action: the snapshot the macrostep began from, its status error, its error actor_failed and its error_value the event’s member error, when it has one. Its children stay, and no xstate.stopChild is returned (XState’s macrostep).

  21. The actions that name other actors are registered by name and returned with XState’s params: spawn_child_action as xstate.spawnChild {id, src, input?, systemId?}, adding the child; stop_child_action as xstate.stopChild, as in guarantee 18; send_to_action and forward_to_action, which sends the microstep’s event, as xstate.sendTo {targetId, event, id?, delay?}; emit_action as xstate.emit {event}. A sendTo’s target is #_parent, #_internal, #system:<systemId>, #_<id> or <id>; a child it names must be in the snapshot’s children when the action resolves, or, named <id> by an entry or initial action, be an invoke of the state being entered (XState’s deferredActorIds); otherwise the microstep fails with unknown_target. A sendTo naming an invoke of the state being entered by its bare id carries action::bound_at: how many of its microstep’s actions come before the point where XState binds it to the child that id names, the end of that state’s entry actions, spawns and initial actions (XState’s retryResolveSendTo); any other action has none.

  22. Every actor a machine names, an invoke’s src or the src of a spawn_child_action the config names as an action, is an entry of implementations::actors; creating the machine refuses any other with unknown_actor. A registered spawn_child_action the config never names is not checked, as XState’s setup accepts it.

  23. A history at the root has no parent to resolve its default against. When its target is absent, a string or a list of strings, the machine is created, as XState creates it, and a microstep that needs the default, the initial one or one that enters the root or reaches it through another history’s default, fails with unknown_target, where XState’s resolveHistoryDefaultTransition throws. A target that is an empty list names nothing, and the root enters nothing below it, as in XState. Creating the machine refuses a target of any other type (guarantee 1).

  24. A value an implementation computes, the value a log logs, an invoke’s input (implementations::inputs, by the invoke’s id), a spawn’s input or a final state’s output (implementations::outputs, by the state’s id, the root’s id for the machine’s), may be none, JavaScript’s undefined: the member is left out, as XState leaves it out, so the log has no value, the child no input and the snapshot no output. An input is computed when its state is entered, after the state’s entry actions; a final state’s output from the context and the event that entered it, and the machine’s from the done event of the final state that completed it, as XState’s getMachineOutput computes it. Creating the machine refuses, with invalid_config, an input that names no invoke or an output that names neither a final state nor the root, and one of either that the config also fixes.

The actor layer

The actor layer’s guarantees are numbered from A1, so that a citation tells them from the machine core’s.

A1

Every host call that delivers (start, send, clock_tick, resolve, reject, resume) opens executions of its own, each with the budget’s fuel, and runs the system until no message is left; it returns out_of_fuel when an actor is left parked, settled otherwise. While an actor is parked, start, send, clock_tick, resolve and reject are checked and then held, in the order made, and return out_of_fuel; resume(), once the parked actors have settled, applies them in that order, each run to its end before the next, as XState runs each of its host’s calls to its end. An answer whose request is no longer pending when it is applied is dropped, as XState ignores a promise that settles after its actor stopped. stop is never held: it is how the host ends an actor that would never settle.

A2

An actor runs one macrostep at a time. An event that arrives while a macrostep has not settled, or before the actor has started, waits; the waiting events run afterwards in the order they arrived, each its own macrostep (XState’s run to completion).

A3

Fuel: every microstep costs one unit of the execution’s fuel, paid before it runs, and so does every message an actor sends (an event, a child’s construction and its reply, a child’s start, a stop) and every timer it arms. When the execution cannot pay, the actor parks with what it has not done; resume() gives each parked actor a new execution and continues it there. A parked actor never runs a microstep or an effect it has not paid for, nor one twice, and does nothing until its resume, so it stays parked only while something is left to do.

A4

Construction and start, after XState’s createActor and start(): an actor’s construction runs its initial macrostep, resolving its actions (A5); its start then hands over the custom actions and logs that macrostep returned and runs what it deferred (A6), the starts of its active children first, as XState’s start() starts the snapshot’s active children and then runs the actor’s initial actions. A child born done or failed starts where its spawn put it. A child is constructed when its parent resolves its spawnChild, and started when the parent’s macrostep has settled; the root is constructed and started by the host’s start. A construction that fails drops what it would have handed over and deferred; the start then fails the actor (A9).

A5

Resolution: as each microstep is computed, the actor resolves its actions in order, as XState’s resolveActionsAndContext does, then the stopChild actions the macrostep returns after its microsteps (guarantee 19):

  • a custom action reaches on_action, and xstate.log on_log;

  • xstate.spawnChild claims its systemId, if any, and creates the child implementations::actors names for its src, a machine actor or a host actor, with its input. A machine child is constructed at once (A4), and the actor waits until it is, so the children of its initial macrostep exist and their systemIds are registered when the next action resolves, as XState’s createActor constructs a child inside resolveSpawn. Its start is deferred;

  • xstate.stopChild releases the systemIds the child and its descendants hold, and the actor forgets the child. A child this macrostep created, which has not started, is stopped at once and never starts; the stop of a started child is deferred. In the initial macrostep all of it is deferred, as XState runs every initial action on start();

  • xstate.sendTo resolves its target now: a child by id or #_<id>, the parent (#_parent), the actor itself (#_internal), or the actor registered under a systemId (#system:<id>, the actor itself when none is, but for a forwardTo, which fails the actor with unknown_target there, as XState’s development build throws). One that carries bound_at (guarantee 21) is bound to the child its id names once that many of the microstep’s actions are resolved, as XState’s retryResolveSendTo binds it, and fails the actor when it is delivered if no child had that id then. Its delivery, or its timer, is deferred;

  • a delayed raise, xstate.cancel and xstate.emit are deferred.

A6

Deferred effects: when its macrostep has settled and every action is resolved, the actor publishes its snapshot (snapshot_of) and runs what the resolution deferred, in order: deliveries and timers, cancels, emits, the stops of started children, the starts of created ones. A started child asked to stop receives the request behind everything already sent it, so it finishes the macrostep it runs and the events that arrived before, ignores what arrives after, asks each of its own children to stop the same way, and stops, running no exit action; a child that has already ended ignores it, and its children keep running, as in XState. The actor then notifies its subscribers when its snapshot is active or done (an empty listener is none, as an empty callback is), releases its systemId if it is done or failed, as XState’s _stopProcedure does even when the report waits for fuel, and reports to its parent: xstate.done.actor.<id> {output, actorId} when done, xstate.error.actor.<id> {error, actorId} when failed, the error being the actor’s error_value (a child’s error climbs unchanged) or the name of its error code.

A7

A delayed raise or sendTo arms a timer owned by its sender, under its id when it has one. A timer armed under an id is the one a cancel of that id finds until another is armed under it or any timer of that id fires; an earlier one under the same id still fires, as XState’s scheduler keeps its timeout and its timerMap names the latest. clock_tick(now) releases the timers due by now one at a time, in (deadline, arming) order, and runs after each, all in one execution, so a timer’s handling can cancel a later one of the same tick, or take its id over, as XState’s SimulatedClock fires them; one armed meanwhile and due by now is released in the same call. When an actor parks, the rest of the tick is held first in line (A1), so no timer fires for a parked actor. A delayed sendTo is forwarded by its sender when the timer fires, in the order the timers fire; a retired actor’s timers never fire.

A8

A systemId names one actor from its creation until the actor is done, failed or stopped, or until a stopChild releases it or one of its ancestors (A5): get finds it, and a claim of it fails the claiming actor (A9). A done or failed actor releases it at once, even while its report waits for fuel (A6), and a child whose construction leaves it done or failed releases its own then, as XState’s constructor does. When its parent starts it, the start fails the parent with system_id_taken if a child the parent’s macrostep created, or one descended from it, holds the name then, as XState’s start() claims it again and throws; it is a deferred effect that fails (A9). XState runs each start and each send of the macrostep’s deferred effects nested, before the next, so a sibling that ends during its own start has released the name by then, and an actor a deferred send reached may have claimed it; here those run later, in turns of their own, so the check sees the names as the macrostep’s own children hold them, and the outcome can differ (Interleaving across actors).

A9

What XState throws for while resolving, a claim of a systemId that is held and a sendTo to the parent of a root, fails the actor there: no later action of the macrostep is handed over or resolved, nothing it deferred runs, the children it created are stopped, and the snapshot is the one the macrostep began from, its status error and its error system_id_taken or unknown_target. What was handed over before stays handed over; the started children it asked to stop are the actor’s again, as the snapshot it falls back to lists them. A microstep that fails, by an implementation or a history at the root, first has the actions it resolved before the failure resolved as any, as a running actor of XState has run them: custom actions and logs are handed over, and a spawn’s claim of a held systemId fails first; then the resolution ends the same way, with the snapshot of guarantee 11. An initial macrostep hands over none of them, as XState runs those on start(). A deferred effect that fails fails the actor with the snapshot its macrostep reached, which an inspector hears as XState’s update inspects it, and the children the macrostep created that have not started are stopped; a start’s active children have.

A10

A host actor is created when its spawnChild resolves and, once started, is a pending host_request; resolve and reject end it, done or failed, and its parent receives its done or error event. A request whose actor has ended is no longer pending, and resolve or reject of it is invalid_argument and changes nothing.

A11

A failed actor’s children keep running, as in XState; a done actor’s were stopped by the stopChild its last macrostep returned. stop stops a root actor and its family from the host, at once; for a child it is invalid_argument and changes nothing, as XState’s actor.stop() refuses a child.

A12

The host’s callbacks, a subscribe or on_emitted listener, the on_action and on_log callbacks and the inspector, run inside an actor’s turn. A call from one of them that changes the system is invalid_argument and changes nothing; the const ones, which read it, are free. subscribe and on_emitted add a listener of one actor, called in the order added, and refuse an address that names no actor with invalid_argument, as start and send do; on_action, on_log and inspect hold one callback for the whole system, which a later call replaces; an empty callback is none.

Reference

Every name xstate declares, in the namespace webcpp::xstate, from the Doc Comments of its headers: each class, function, type and enumerator with its declaration, what it does, what it asks of its caller and how it fails, and the part of XState 5.33.2 it ports. The other sections teach the library; this one is where a name is looked up. XState’s names, in From XState to C++, goes the other way, from XState’s names to xstate’s.

Macros

Name Description

WEBCPP_XSTATE_NO_EXCEPTIONS

Defined when xstate is built without exceptions: when webcpp has none (the compiler has none, or the developer defined WEBCPP_NO_EXCEPTIONS for every webcpp library), or when the developer defines it alone, to disable xstate’s in a build that has them.

webcpp::xstate

Types

Name Description

action

An action returned to the caller to execute.

action_args

What an implementation is handed, built by xstate for the call.

action_ref

An action as the config names it, or one of the two XState adds to a state with after.

actor_options

How an actor the host creates starts, as XState’s createActor options say.

actor_record

What the layer keeps of one actor.

actor_system

A system of actors on one xactor scheduler, run by its host.

assign_action

An assign, which merges a partial update into the context.

cancel_action

A cancel of a delayed raise or send.

construct_actor

Constructs a machine child: it runs its initial macrostep and resolves its actions, then answers constructed.

constructed

A child’s answer to construct_actor, once its own children are constructed too.

delayed

A delayed event to the actor itself, armed under an id.

emit_action

An event emitted to the listeners of the actor, not to a machine.

event

An event: its type, and the members that travel with it.

forward_to_action

A send of the microstep’s event to another actor.

guard_node

A guard as the config names it: a registered guard, by name, with its params, or one of XState’s higher-order guards.

host_actor

An actor whose work is done outside the machines, by the host.

host_answer

The host’s answer to a host actor: its output, or its error.

host_logic

The logic of a host actor, in place of XState’s fromPromise.

host_request

A host actor’s request, pending until the host resolves or rejects it.

implementations

The implementations a machine is created with, each registered by the name its config gives it.

inspector

What an inspector hears, after XState’s inspect.

invoke_definition

A child actor a state invokes, which lives while the state is active.

log_action

A log of a value, with an optional label.

machine

The immutable tree of state nodes create_machine builds from a config, with the implementations it was given.

machine_actor

A machine run as a child actor, with that machine’s own implementations.

machine_logic

The logic of a machine actor, XState’s createActor for a machine.

macrostep

The cursor of one macrostep, which runs one microstep a call.

macrostep_result

A settled macrostep: its stable snapshot, every action its microsteps returned, and how many microsteps ran.

microstep

One microstep: the event it ran for, the transitions it took, the actions it returned and the snapshot it left.

node_set

A set of node indices that keeps the order they were added in.

progress

What one call of macrostep::next did: its microstep, and whether it settled the macrostep.

raise_action

A raise, which sends an event to the machine itself.

relay

A delayed sendTo whose timer fired: its sender forwards event to to.

resume

Continues an actor the fuel of an earlier execution could not finish.

send_parent_action

A send to the machine’s parent actor.

send_to_action

A send of an event to another actor, named by a target string.

simulated_clock

A clock the caller owns, which holds the delayed events of the machine core until the time it is set to reaches them.

snapshot

Where a machine is, as a plain value to keep, copy or store.

spawn_child_action

A spawn of a child actor that lives until it is stopped or the machine is done.

start_actor

Starts the actor, constructing it first if no parent did.

state_node

One node of a machine.

stop_actor

Asks an actor to stop with its family, once it has handled what its parent sent it before.

stop_child_action

A stop of a child actor.

system_state

What an actor system knows beside xactor’s statuses, shared by every actor of the system.

transition_definition

One transition of a node.

Type Aliases

Name Description

action_implementation

The built-in action an entry of implementations::actions stands for.

actor_implementation

What an invoke’s or a spawnChild’s src names, registered in implementations::actors.

actor_message

Every message an actor of the layer receives.

assigner

Returns the partial update an assign merges into the context.

context_maker

Returns the initial context from the input the machine was started with.

delay_maker

Returns a delay, in milliseconds.

delay_ref

A delay as an action names it: a number of milliseconds, or the name of a delay of the registry.

event_maker

Returns the event a raise, a sendTo, a sendParent or an emit sends.

id_maker

Computes a spawned child’s id.

predicate

Decides whether a transition is taken.

result

What a function that can fail returns: a T, or the error_code of the failure.

value_maker

Returns a value, or none for JavaScript’s undefined, which leaves the member out.

Enums

Name Description

errc

The errors xstate reports, as boost::system::error_code values of the category error_category.

history_kind

A history node’s kind, from its history member.

node_type

A node’s type, as XState’s StateNode decides it.

run_outcome

What a host call left: every actor settled, or one parked for want of fuel.

status

A snapshot’s status.

Functions

Name Description

begin

Makes the cursor of the macrostep an event runs from a snapshot.

begin_initial

Makes the cursor of a machine’s initial macrostep.

can

Whether an event would take, from a snapshot, a transition that does something.

create_machine

Builds a machine from the JSON of XState’s createMachine config, with the implementations its names stand for.

create_machine_from_definition

Loads a machine back from a definition, with the implementations and the context, which a definition does not hold.

ending_event

The event an actor that ends sends its parent.

error_category

The category of every errc.

event_from_json

Reads an event from XState’s flat JSON form.

failure

Builds a failed result of the type the call would have returned.

get_initial_microsteps

Runs a machine’s initial macrostep and returns every microstep.

get_initial_snapshot

Runs a machine’s initial macrostep and returns the snapshot it starts in.

get_meta

The meta of every active node that has one, by node id.

get_microsteps

Runs the macrostep of an event and returns every microstep.

get_next_snapshot

Runs the macrostep of an event and returns the snapshot it leads to.

get_next_transitions

Lists every transition a snapshot could take, guards not evaluated.

initial_transition

Runs a machine’s initial macrostep and returns its snapshot and its actions.

make_error_code

Makes the error_code of an errc, as its implicit conversion does.

matches_state

Whether the state value child is parent or a state within it.

path_to_state_value

Builds the state value a path describes.

resolve_state

Builds the snapshot of a state value, completed to every node it implies, with a context.

to_json

to_json overloads

to_state_path

Splits a state id into the keys of its path.

to_state_value

Converts a state value given as a dotted string to its object form.

transition

Runs the macrostep of an event and returns the snapshot it leads to and its actions.

Variables

Name Description

init_event_type

The type of the event every initial macrostep runs with, "xstate.init".

wildcard

The descriptor that matches every event, "*".

Using Declarations

Name Description

actor_ref

An actor’s address in its system: webcpp::xactor::actor_ref, brought into this namespace.

budgets

The fuel of one execution of the system: webcpp::xactor::budgets, brought into this namespace.

webcpp::xstate::action_implementation

The built-in action an entry of implementations::actions stands for.

Synopsis

using action_implementation = std::variant<assign_action, raise_action, log_action, send_parent_action, cancel_action, spawn_child_action, stop_child_action, send_to_action, forward_to_action, emit_action>;

Description

A name the config gives and the map does not hold is a custom action, returned to the caller as it was named.

See Also

"Built-in and custom actions", in the guide.

webcpp::xstate::actor_implementation

What an invoke’s or a spawnChild’s src names, registered in implementations::actors.

Synopsis

using actor_implementation = std::variant<machine_actor, host_actor>;

Description

It is what XState’s setup({ actors }) registers: a machine, or the work of the host.

See Also

"Invoking actors", in the guide.

webcpp::xstate::actor_message

Every message an actor of the layer receives.

Synopsis

using actor_message = std::variant<start_actor, event, resume, relay, host_answer, stop_actor, construct_actor, constructed, delayed>;

Description

A host does not build them: the calls of actor_system deliver them, and the logic classes send them to each other. Its alternatives are start_actor, an event to run as a macrostep, resume, relay, host_answer, stop_actor, construct_actor, constructed and delayed.

Note

xactor names an alternative in its log by its index, so their order is part of a recorded run.

See Also

"How the layer runs", in the guide.

webcpp::xstate::assigner

Returns the partial update an assign merges into the context.

Synopsis

using assigner = std::function<result<boost::json::object>(action_args const&)>;

Description

It is what XState’s assign takes. A failure fails the macrostep that called it, which settles with the snapshot it began from, its status status::error and the failure as its error.

See Also

"Updating context with assign", in the guide.

webcpp::xstate::context_maker

Returns the initial context from the input the machine was started with.

Synopsis

using context_maker = std::function<result<boost::json::value>(boost::json::value const&)>;

Description

It ports XState’s context: ({ input }) => .... A failure, or a value that is not an object (errc::implementation_failed), fails the initial macrostep.

See Also

"Initial context from the input", in the guide.

webcpp::xstate::delay_maker

Returns a delay, in milliseconds.

Synopsis

using delay_maker = std::function<result<uint64_t>(action_args const&)>;

Description

It ports a delay function of XState’s setup({ delays }). A failure fails the macrostep that called it, as an assigner’s does.

See Also

"Delayed transitions", in the guide.

webcpp::xstate::delay_ref

A delay as an action names it: a number of milliseconds, or the name of a delay of the registry.

Synopsis

using delay_ref = std::variant<uint64_t, std::string>;

Description

A name is the name of an entry of implementations::delays, which computes the delay when the action resolves. A name the registry does not hold is no delay, as in XState: the raise is queued at once, and the send is returned without a delay. An after key names a delay the same way, but create_machine refuses one naming no registered delay, with errc::unknown_delay.

See Also

"Delayed raise, ids and cancel", in the guide.

webcpp::xstate::event_maker

Returns the event a raise, a sendTo, a sendParent or an emit sends.

Synopsis

using event_maker = std::function<result<event>(action_args const&)>;

Description

A failure fails the macrostep that called it, as an assigner’s does.

See Also

"Action objects and params", in the guide.

webcpp::xstate::id_maker

Computes a spawned child’s id.

Synopsis

using id_maker = std::function<result<std::string>(action_args const&)>;

Description

It ports the id function of XState’s spawnChild. A failure fails the macrostep that called it, as an assigner’s does.

See Also

"Input to a spawned actor", in the guide.

webcpp::xstate::predicate

Decides whether a transition is taken.

Synopsis

using predicate = std::function<result<bool>(action_args const&)>;

Description

It ports a guard function of XState’s setup({ guards }). A failure fails the macrostep that evaluated it, as an assigner’s does, and a call to can too.

See Also

"Guards", in the guide.

webcpp::xstate::result

What a function that can fail returns: a T, or the error_code of the failure.

Synopsis

Declared in <webcpp/xstate/errors.hpp>

template<class T>
using result = boost::system::result<T, boost::system::error_code>;

Description

It is Boost.System’s result. Test it with has_value() before reading the value: * of a failed result is undefined, and its value() reports the failure through boost::throw_exception, which a program built without exceptions defines to end the program.

Template Parameters

Name Description

T

The type of the value; void for a function that returns none.

See Also

"Errors as values", in the guide.

webcpp::xstate::value_maker

Returns a value, or none for JavaScript’s undefined, which leaves the member out.

Synopsis

using value_maker = std::function<result<std::optional<boost::json::value>>(action_args const&)>;

Description

It computes the value a log logs, the input a child starts with, an invoke’s input (implementations::inputs) and a state’s or the machine’s output (implementations::outputs). A failure fails the macrostep that called it, as an assigner’s does.

See Also

"When a value is undefined", in the guide.

webcpp::xstate::action

An action returned to the caller to execute.

Synopsis

struct action;

Description

It ports XState’s ExecutableActionObject. A custom action (builtin false) has the type the config named and the params it gave, null when it gave none. A built-in one was resolved by xstate, and params holds what it resolved: an event in XState’s flat form, a delay in milliseconds.

Each built-in type is returned for these actions, with these params:

  • xstate.raise, for a raise_action and on entering a state with after, its params event, id when it has one and delay when delayed;

  • xstate.cancel, for a cancel_action and on leaving a state with after, its params sendId;

  • xstate.log, for a log_action, its params value unless undefined and label when it has one;

  • xstate.sendTo, for a send_to_action, a send_parent_action and a forward_to_action, its params targetId, event, id when it has one and delay when delayed;

  • xstate.emit, for an emit_action, its params event;

  • xstate.spawnChild, for a spawn_child_action and on entering a state that invokes, its params id, src, and input and systemId when there are any;

  • xstate.stopChild, for a stop_child_action, on leaving a state that invokes, and for each child left when a macrostep settles not active, its params id, or null when the child was not there.

An assign is never returned.

Note

builtin tells the two kinds apart, not the type: a config may name an action xstate.sendTo and give it no implementation, and it is returned as a custom action all the same, which simulated_clock::apply ignores.

Data Members

Name Description

bound_at

Where XState binds a sendTo that names, by its bare id, an invoke of the state being entered.

builtin

Whether xstate resolved the action, as a raise, a sendTo or a spawnChild does, as opposed to a custom action returned as the config named it.

forwarded

Whether the action is a forwardTo’s sendTo.

params

The resolved params of a built-in, or the params the config gave, null when none.

type

The action’s type: the name the config gave, or a built-in’s xstate. name.

See Also

"Built-in and custom actions", in the guide.

"Custom actions", in the guide.

webcpp::xstate::action::bound_at

Where XState binds a sendTo that names, by its bare id, an invoke of the state being entered.

Synopsis

std::optional<std::size_t> bound_at = {};

Description

It counts how many of its microstep’s actions come before the point where XState binds the sendTo to the child that id names: the end of that state’s entry actions, spawns and initial actions (XState’s retryResolveSendTo). When no child has that id at that point, the actor layer fails the actor with errc::unknown_target where XState throws.

See Also

"A sendTo whose child is gone", in the guide.

webcpp::xstate::action::builtin

Whether xstate resolved the action, as a raise, a sendTo or a spawnChild does, as opposed to a custom action returned as the config named it.

Synopsis

bool builtin = false;

webcpp::xstate::action::forwarded

Whether the action is a forwardTo’s sendTo.

Synopsis

bool forwarded = false;

Description

XState’s development build refuses to resolve a forwardTo to no actor, where a sendTo goes to the actor itself; the actor layer reads it.

webcpp::xstate::action::params

The resolved params of a built-in, or the params the config gave, null when none.

Synopsis

boost::json::value params = {};

webcpp::xstate::action::type

The action’s type: the name the config gave, or a built-in’s xstate. name.

Synopsis

std::string type = {};

webcpp::xstate::action_args

What an implementation is handed, built by xstate for the call.

Synopsis

struct action_args;

Description

Every implementation but implementations::context is handed one. The references live during the call.

Data Members

Name Description

context

The context as it is when the implementation runs, after the assigns that ran before it in the microstep.

event

The microstep’s event.

params

The action’s or the guard’s params as the config wrote them.

See Also

"How guards and actions read the context", in the guide.

webcpp::xstate::action_args::context

The context as it is when the implementation runs, after the assigns that ran before it in the microstep.

Synopsis

boost::json::value const& context;

webcpp::xstate::action_args::event

The microstep’s event.

Synopsis

xstate::event const& event;

Description

For the machine’s output, computed by implementations::outputs under the root’s id, it is the done event of the final state that completed the machine.

webcpp::xstate::action_args::params

The action’s or the guard’s params as the config wrote them.

Synopsis

boost::json::value const& params;

Description

It is null when the config gave none, and for an input or an output.

webcpp::xstate::action_ref

An action as the config names it, or one of the two XState adds to a state with after.

Synopsis

struct action_ref;

Description

A kind::named action has its type, its params, null when it has none, and its JSON as written in config. The two an after adds are those XState’s getDelayedTransitions adds: the delayed raise of its entry (kind::after_raise), whose type is the after event’s, xstate.after.<delay>.<state id>, which is also the raise’s id, with its delay; and the cancel of its exit (kind::after_cancel).

Enums

Name Description

kind

Where an action comes from: the config, or an after.

Data Members

Name Description

config

The action as the config wrote it, for the definition.

delay

The delay of an after_raise.

kind

Where this action comes from.

params

The action’s params, null when the config gave none.

type

The action’s name, or, for the two after actions, the after event’s type, which is also the id of the delayed raise.

See Also

"Entry, exit and transition actions", in the guide.

webcpp::xstate::action_ref::kind

Where an action comes from: the config, or an after.

Synopsis

enum class kind : int;

Members

Name Description

named

An action the config names.

after_raise

The delayed raise an after adds to its state’s entry.

after_cancel

The cancel an after adds to its state’s exit.

webcpp::xstate::action_ref::kind::named

An action the config names.

Synopsis

named

webcpp::xstate::action_ref::kind::after_raise

The delayed raise an after adds to its state’s entry.

Synopsis

after_raise

webcpp::xstate::action_ref::kind::after_cancel

The cancel an after adds to its state’s exit.

Synopsis

after_cancel

webcpp::xstate::action_ref::config

The action as the config wrote it, for the definition.

Synopsis

boost::json::value config = {};

webcpp::xstate::action_ref::delay

The delay of an after_raise.

Synopsis

std::optional<delay_ref> delay = {};

webcpp::xstate::action_ref::kind

Where this action comes from.

Synopsis

kind kind = kind::named;

webcpp::xstate::action_ref::params

The action’s params, null when the config gave none.

Synopsis

boost::json::value params = {};

webcpp::xstate::action_ref::type

The action’s name, or, for the two after actions, the after event’s type, which is also the id of the delayed raise.

Synopsis

std::string type = {};

webcpp::xstate::actor_options

How an actor the host creates starts, as XState’s createActor options say.

Synopsis

struct actor_options;

Description

Data Members

Name Description

id

The actor’s id, which actor_system::id_of returns.

input

The actor’s input, null for none.

system_id

The systemId the system registers the actor under, from its creation until it ends.

See Also

"Options", in the guide.

webcpp::xstate::actor_options::id

The actor’s id, which actor_system::id_of returns.

Synopsis

std::optional<std::string> id = {};

Description

When none is given, it is the actor’s session id x:<n>, where n counts the actors the system created or began to create before it, as XState’s createActor names a root; XState counts across every system of the process, and xstate within one actor_system.

See Also

"One system per actor_system", in the guide.

webcpp::xstate::actor_options::input

The actor’s input, null for none.

Synopsis

boost::json::value input = {};

Description

The machine reads it in implementations::context, and the event xstate.init of its initial macrostep carries it as its member input when it is not null.

See Also

"A machine’s input", in the guide.

webcpp::xstate::actor_options::system_id

The systemId the system registers the actor under, from its creation until it ends.

Synopsis

std::optional<std::string> system_id = {};

Description

None, or an empty string, registers nothing, as XState registers only a truthy systemId. actor_system::get finds the actor by it.

See Also

"The system", in the guide.

"Guarantees", in the guide: guarantee A8.

webcpp::xstate::actor_record

What the layer keeps of one actor.

Synopsis

struct actor_record;

Description

system_state holds one for each actor, from its creation on.

Data Members

Name Description

children

The children it spawned and still has, by id, in spawning order.

current

A machine actor’s snapshot, once its first macrostep has settled, which actor_system::snapshot_of returns.

id

The id its parent knows it by, an invoke’s or a spawnChild’s id, or the host’s id for a root.

parent

The actor that spawned it; none for an actor the host created.

src

The implementation it runs, by its name in implementations::actors; empty for a root.

system_id

The systemId it claimed when it was created, if any, which it may have released since.

webcpp::xstate::actor_record::children

The children it spawned and still has, by id, in spawning order.

Synopsis

std::vector<std::pair<std::string, xactor::actor_ref>> children = {};

webcpp::xstate::actor_record::current

A machine actor’s snapshot, once its first macrostep has settled, which actor_system::snapshot_of returns.

Synopsis

std::optional<snapshot> current = {};

webcpp::xstate::actor_record::id

The id its parent knows it by, an invoke’s or a spawnChild’s id, or the host’s id for a root.

Synopsis

std::string id = {};

webcpp::xstate::actor_record::parent

The actor that spawned it; none for an actor the host created.

Synopsis

std::optional<xactor::actor_ref> parent = {};

webcpp::xstate::actor_record::src

The implementation it runs, by its name in implementations::actors; empty for a root.

Synopsis

std::string src = {};

webcpp::xstate::actor_record::system_id

The systemId it claimed when it was created, if any, which it may have released since.

Synopsis

std::optional<std::string> system_id = {};

webcpp::xstate::actor_system

A system of actors on one xactor scheduler, run by its host.

Synopsis

class actor_system;

Description

It ports XState’s createActor and its actor system in one. Its scheduler, a webcpp::xactor::scheduler, holds one first-in first-out mailbox per actor and one first-in first-out queue of the actors that have work, and each turn hands one message to its actor, so a message an actor sends is queued, never handled inside the sender’s turn. The host creates actors from machines, which runs nothing. Then it starts them, sends them events, brings the time and answers the host actors: each of these calls, and actor_system::resume, runs the system until no message is left, as XState’s send() processes at once, and returns a run_outcome. stop stops a family at once and returns it. The system is neither copied nor moved, since every actor holds a reference to its shared state, and one thread runs it.

Every refusal is webcpp::xactor::errc::invalid_argument, of the category "webcpp.xactor", but one: create_actor refuses a systemId another actor holds with xstate’s own errc::system_id_taken, of the category "webcpp.xstate". xstate’s errc has no invalid_argument. The readers refuse nothing: snapshot_of, parent_of, id_of, get and child_of answer nullptr, none or an empty view for what they do not find.

While an actor is parked, start, send, clock_tick, resolve and reject are checked, then held in the order made, and return run_outcome::out_of_fuel; actor_system::resume applies them once the parked actors have settled, each run to its end before the next, as XState runs each of its host’s calls to its end. stop is never held.

The host’s callbacks, the listeners of subscribe and on_emitted, those of on_action and on_log, and the inspector, run inside an actor’s turn, while the system runs. A call from one of them to a member that changes the system, a registration included, returns webcpp::xactor::errc::invalid_argument and changes nothing; the const members, which read the system, are free. XState accepts a send from a subscriber and handles it after the update in progress. An empty std::function is no callback.

Member Functions

Name Description

actor_system [constructor]

Constructors

~actor_system [destructor]

Destroys the system and every actor in it.

operator=

Assignment operators

child_of

The child an actor knows by an id, while it has it.

clock_tick

Moves the system’s time to now, releasing the timers due by then.

create_actor

Creates an actor that runs a machine, not yet started.

get

The actor registered under a systemId, while it holds it.

host_requests

The pending requests of the host actors, in the order their actors started.

id_of

The id an actor’s parent knows it by, or the host’s id for it.

inspect

Hands the system’s inspector what every actor does.

on_action

Hands every custom action of every actor to a listener, with the actor that returned it.

on_emitted

Adds a listener of the events an actor emits.

on_log

Hands every xstate.log of every actor to a listener, with the actor that returned it.

parent_of

The actor that spawned an actor.

reject

Answers a pending request with an error, and runs the system.

resolve

Answers a pending request with an output, and runs the system.

resume

Gives every parked actor a new execution and runs, then applies the host’s calls held while an actor was parked.

run_until_idle

Runs the system until no message is left.

send

Sends an actor an event from the host, and runs the system.

snapshot_of

A machine actor’s snapshot, once it has been constructed.

start

Starts an actor, constructing it first, and runs the system.

status_of

Where an actor is in its life, as its scheduler keeps it.

stop

Stops a root actor and its whole family at once.

subscribe

Adds a listener of the snapshots an actor settles in.

See Also

"Actors", in the guide.

"Creating an actor", in the guide.

"A callback that calls the system back", in the guide.

"Guarantees", in the guide: guarantees A1 and A12.

webcpp::xstate::actor_system::actor_system

Constructors

Synopses

Not copyable: every actor holds a reference to the system’s state.

actor_system(actor_system const&) = delete;

Not movable: every actor holds a reference to the system’s state.

actor_system(actor_system&&) = delete;

Makes a system with no actor, whose every execution starts with limits.fuel.

explicit
actor_system(xactor::budgets limits);

Parameters

Name Description

limits

The fuel of each execution.

webcpp::xstate::actor_system::actor_system

Not copyable: every actor holds a reference to the system’s state.

Synopsis

actor_system(actor_system const&) = delete;

webcpp::xstate::actor_system::actor_system

Not movable: every actor holds a reference to the system’s state.

Synopsis

actor_system(actor_system&&) = delete;

webcpp::xstate::actor_system::actor_system

Makes a system with no actor, whose every execution starts with limits.fuel.

Synopsis

explicit
actor_system(xactor::budgets limits);

Parameters

Name Description

limits

The fuel of each execution.

webcpp::xstate::actor_system::~actor_system

Destroys the system and every actor in it.

Synopsis

~actor_system() = default;

webcpp::xstate::actor_system::operator=

Assignment operators

Synopses

Not copy-assignable: every actor holds a reference to the system’s state.

actor_system&
operator=(actor_system const&) = delete;

Not move-assignable: every actor holds a reference to the system’s state.

webcpp::xstate::actor_system::operator=

Not copy-assignable: every actor holds a reference to the system’s state.

Synopsis

actor_system&
operator=(actor_system const&) = delete;

webcpp::xstate::actor_system::operator=

Not move-assignable: every actor holds a reference to the system’s state.

Synopsis

actor_system&
operator=(actor_system&&) = delete;

webcpp::xstate::actor_system::child_of

The child an actor knows by an id, while it has it.

Synopsis

[[nodiscard]]
std::optional<xactor::actor_ref>
child_of(
    xactor::actor_ref parent,
    std::string_view id) const;

Return Value

The child; none when parent has no child of that id, or names no actor.

Note

The return value should not be discarded.

Parameters

Name Description

parent

The actor.

id

The child’s id, its invoke’s or its spawnChild’s.

See Also

"Invoking actors", in the guide.

webcpp::xstate::actor_system::clock_tick

Moves the system’s time to now, releasing the timers due by then.

Synopsis

result<run_outcome>
clock_tick(uint64_t now);

Description

It releases them one at a time, in (deadline, arming) order, and runs the system after each, all in one execution, whose fuel bounds a chain of timers that keep arming. A timer armed meanwhile and due by now is released in the same call, where XState’s may not be. A delayed sendTo is forwarded by its sender when its timer fires. When an actor parks, the rest of the tick is held first in line, so no timer fires for a parked actor.

A time earlier than the last tick’s is not refused: the system’s time moves back to it, and a timer armed afterwards falls due that much earlier. simulated_clock::set refuses the same move, and XState’s SimulatedClock.set throws.

Note

One at a time, as XState’s SimulatedClock fires them, so a timer’s handling can cancel a later one of the same tick, or take its id over.

Return Value

The outcome of the run; webcpp::xactor::errc::invalid_argument from a callback.

Parameters

Name Description

now

The time, in milliseconds.

See Also

"Delayed events and time", in the guide.

"A timer that falls due during clock_tick", in the guide.

"A tick earlier than the last", in the guide.

"Guarantees", in the guide: guarantee A7.

webcpp::xstate::actor_system::create_actor

Creates an actor that runs a machine, not yet started.

Synopsis

result<xactor::actor_ref>
create_actor(
    machine const& logic,
    actor_options const& options = {});

Description

It ports XState’s createActor. Unlike XState’s, the actor’s construction, which runs its initial macrostep, happens on start, so until then snapshot_of returns nullptr and get finds none of its children’s systemIds. The actor holds its systemId from its creation, before it starts, until it is done, fails or is stopped. The roots of one system share their systemIds, where XState gives each root a system of its own.

Return Value

The actor’s address; errc::system_id_taken when an actor of the system holds options.system_id, which creates nothing; or webcpp::xactor::errc::invalid_argument from a callback.

Parameters

Name Description

logic

The machine the actor runs.

options

The actor’s input, id and systemId.

See Also

"Creating an actor", in the guide.

"The root is constructed on start", in the guide.

"One system per actor_system", in the guide.

"Guarantees", in the guide: guarantee A8.

webcpp::xstate::actor_system::get

The actor registered under a systemId, while it holds it.

Synopsis

[[nodiscard]]
std::optional<xactor::actor_ref>
get(std::string_view system_id) const;

Description

It ports XState’s system.get(systemId), over every root of the system and their families. An actor holds its systemId from its creation until it is done, fails or is stopped, or until a stopChild releases it or one of its ancestors.

Return Value

The actor; none when no actor holds system_id.

Note

The return value should not be discarded.

Parameters

Name Description

system_id

The systemId.

See Also

"The system", in the guide.

"Guarantees", in the guide: guarantee A8.

webcpp::xstate::actor_system::host_requests

The pending requests of the host actors, in the order their actors started.

Synopsis

[[nodiscard]]
std::vector<host_request>
host_requests() const;

Description

A request is pending from its host actor’s start until resolve or reject answers it, or its actor is stopped.

Return Value

A copy of the pending requests.

Note

The return value should not be discarded.

See Also

"Host actors", in the guide.

"Guarantees", in the guide: guarantee A10.

webcpp::xstate::actor_system::id_of

The id an actor’s parent knows it by, or the host’s id for it.

Synopsis

[[nodiscard]]
std::string_view
id_of(xactor::actor_ref actor) const;

Description

A child’s id is its invoke’s or its spawnChild’s; a root’s is the actor_options::id it was created with, or its session id.

Return Value

The id, valid while the system lives; empty for an address that names no actor.

Note

The return value should not be discarded.

Parameters

Name Description

actor

The actor.

See Also

"Options", in the guide.

webcpp::xstate::actor_system::inspect

Hands the system’s inspector what every actor does.

Synopsis

result<void>
inspect(inspector heard);

Description

It ports XState’s inspect option: the inspector hears each actor’s start, each macrostep a machine actor settles and each event an actor emits. It replaces the previous inspector.

Return Value

Success; webcpp::xactor::errc::invalid_argument from a callback.

Parameters

Name Description

heard

The inspector, whose empty members are never called.

See Also

"Inspection", in the guide.

"Guarantees", in the guide: guarantee A12.

webcpp::xstate::actor_system::on_action

Hands every custom action of every actor to a listener, with the actor that returned it.

Description

An action is handed over as its microstep is resolved, and an actor’s initial ones when it starts. A macrostep that fails hands over those it resolved before the failure, as a running actor of XState has run them, and none after an action XState would throw for. An initial macrostep that fails hands over none, as XState’s start of an actor whose initial snapshot failed runs none of its initial actions. This is how a custom action runs, since a JSON config holds no function. It replaces the previous listener, and an empty one removes it.

Return Value

Success; webcpp::xactor::errc::invalid_argument from a callback.

Parameters

Name Description

listener

The listener.

See Also

"Custom actions", in the guide.

"Guarantees", in the guide: guarantees A5 and A12.

webcpp::xstate::actor_system::on_emitted

Adds a listener of the events an actor emits.

Synopsis

result<void>
on_emitted(
    xactor::actor_ref actor,
    system_state::event_listener listener);

Description

It ports XState’s actor.on('*', handler): the listener hears each event the actor emits, in order, after the inspector. An event of the type * reaches it once, where XState’s listener hears it twice. Each call adds a listener, and an actor’s listeners hear in the order they were added. An empty listener is none.

Return Value

Success; webcpp::xactor::errc::invalid_argument for an address that names no actor, and from a callback.

Parameters

Name Description

actor

The actor.

listener

The listener.

See Also

"Watching an actor", in the guide.

"Listeners of emitted events", in the guide.

"Guarantees", in the guide: guarantee A12.

webcpp::xstate::actor_system::on_log

Hands every xstate.log of every actor to a listener, with the actor that returned it.

Description

The listener receives the log’s value, nullptr for JavaScript’s undefined and valid during the call, and its label, empty when it has none. A log is handed over when a custom action would be, as on_action says. It replaces the previous listener, and an empty one removes it.

Return Value

Success; webcpp::xactor::errc::invalid_argument from a callback.

Parameters

Name Description

listener

The listener.

See Also

"Log", in the guide.

"Guarantees", in the guide: guarantees A5 and A12.

webcpp::xstate::actor_system::parent_of

The actor that spawned an actor.

Synopsis

[[nodiscard]]
std::optional<xactor::actor_ref>
parent_of(xactor::actor_ref actor) const;

Return Value

Its parent, which invoked or spawned it; none for an actor the host created, and for an address that names no actor.

Note

The return value should not be discarded.

Parameters

Name Description

actor

The actor.

See Also

"Invoking actors", in the guide.

webcpp::xstate::actor_system::reject

Answers a pending request with an error, and runs the system.

Synopsis

result<run_outcome>
reject(
    host_request const& request,
    std::optional<boost::json::value> error);

Description

The request’s host actor fails with error, its xactor status then error, and its parent receives xstate.error.actor.<id> with error and actorId. An answer held while an actor is parked is dropped as resolve says.

Return Value

The outcome of the run; webcpp::xactor::errc::invalid_argument, and nothing changes, when the request is no longer pending, its actor having been answered or stopped, or from a callback.

Parameters

Name Description

request

The request, as host_requests lists it.

error

The error, none for JavaScript’s undefined, which the error event then leaves out.

See Also

"Host actors", in the guide.

"Guarantees", in the guide: guarantee A10.

webcpp::xstate::actor_system::resolve

Answers a pending request with an output, and runs the system.

Synopsis

result<run_outcome>
resolve(
    host_request const& request,
    std::optional<boost::json::value> output);

Description

The request’s host actor finishes done with output, and its parent receives xstate.done.actor.<id> with output and actorId. An answer held while an actor is parked is dropped if its request is no longer pending when it is applied, as XState ignores a promise that settles after its actor stopped; the call that held it has already returned run_outcome::out_of_fuel.

Return Value

The outcome of the run; webcpp::xactor::errc::invalid_argument, and nothing changes, when the request is no longer pending, its actor having been answered or stopped, or from a callback.

Parameters

Name Description

request

The request, as host_requests lists it.

output

The output, none for JavaScript’s undefined, which the done event then leaves out.

See Also

"Host actors", in the guide.

"Guarantees", in the guide: guarantee A10.

webcpp::xstate::actor_system::resume

Gives every parked actor a new execution and runs, then applies the host’s calls held while an actor was parked.

Description

Each new execution starts with the budget’s fuel. The held calls are applied in the order they were made, each run to its end before the next, until an actor parks again.

Return Value

The outcome of the run; webcpp::xactor::errc::invalid_argument from a callback.

See Also

"Fuel, parking and resume", in the guide.

"Guarantees", in the guide: guarantees A1 and A3.

webcpp::xstate::actor_system::run_until_idle

Runs the system until no message is left.

Synopsis

result<run_outcome>
run_until_idle();

Description

Every call that returns a run_outcome already runs the system so; this one delivers nothing, opens no execution and gives no fuel.

Return Value

run_outcome::out_of_fuel while an actor is parked, run_outcome::settled otherwise; webcpp::xactor::errc::invalid_argument from a callback.

See Also

"Fuel, parking and resume", in the guide.

webcpp::xstate::actor_system::send

Sends an actor an event from the host, and runs the system.

Synopsis

result<run_outcome>
send(
    xactor::actor_ref actor,
    event happened);

Description

It ports XState’s actor.send(event). An actor that has not started, or is in a macrostep, keeps the event until it can run it, each event a macrostep of its own in the order it arrived; an actor that has ended drops it, as XState drops an event sent to a stopped actor.

Return Value

The outcome of the run; webcpp::xactor::errc::invalid_argument when actor names no actor of the system, or from a callback.

Parameters

Name Description

actor

The actor.

happened

The event.

See Also

"Create an actor and send events", in the guide.

"Guarantees", in the guide: guarantee A2.

webcpp::xstate::actor_system::snapshot_of

A machine actor’s snapshot, once it has been constructed.

Synopsis

[[nodiscard]]
snapshot const*
snapshot_of(xactor::actor_ref actor) const;

Description

It ports XState’s actor.getSnapshot(): the snapshot the actor last settled in, which it publishes when its macrostep has settled and its actions are resolved.

Return Value

The snapshot, valid while the system lives, what it points to changing when the actor settles again; nullptr before the actor’s construction, for a host actor and for an address that names no actor.

Note

The return value should not be discarded.

Parameters

Name Description

actor

The actor.

See Also

"Snapshots and status", in the guide.

webcpp::xstate::actor_system::start

Starts an actor, constructing it first, and runs the system.

Synopsis

result<run_outcome>
start(xactor::actor_ref actor);

Description

It ports XState’s actor.start(). The construction runs the actor’s initial macrostep and resolves its actions; the start then hands the custom actions and logs of that macrostep to the callbacks, starts the actor’s active children and runs what the macrostep deferred. When the initial macrostep fails, the start hands over none of them and the actor fails. A second start does nothing.

Return Value

The outcome of the run; webcpp::xactor::errc::invalid_argument when actor names no actor of the system, or from a callback.

Parameters

Name Description

actor

The actor, which create_actor made.

See Also

"Creating an actor", in the guide.

"Guarantees", in the guide: guarantee A4.

webcpp::xstate::actor_system::status_of

Where an actor is in its life, as its scheduler keeps it.

Synopsis

[[nodiscard]]
result<xactor::status>
status_of(xactor::actor_ref actor) const;

Description

The status is a webcpp::xactor::status: active, done, error or stopped. A machine actor that fails is error, and its snapshot’s error says why.

Return Value

The actor’s status; webcpp::xactor::errc::invalid_argument when actor names no actor of the system.

Note

The return value should not be discarded.

Parameters

Name Description

actor

The actor.

See Also

"Snapshots and status", in the guide.

webcpp::xstate::actor_system::stop

Stops a root actor and its whole family at once.

Synopsis

result<std::vector<xactor::actor_ref>>
stop(xactor::actor_ref actor);

Description

It ports XState’s actor.stop(). Each member is stopped after its descendants, the children of a failed member included, where XState’s leaves them running. No exit action runs, and a stopped actor’s timers never fire, where XState may still fire one.

Note

It is never held behind a parked actor, since stopping is how the host ends an actor that would never settle.

Return Value

The actors it stopped, each after its descendants; webcpp::xactor::errc::invalid_argument, and nothing changes, for a child, which XState refuses to stop directly, for an address that names no actor, and from a callback.

Parameters

Name Description

actor

The root actor, one the host created.

See Also

"The host’s stop of a family with a failed member", in the guide.

"Timers of an ended actor", in the guide.

"Guarantees", in the guide: guarantee A11.

webcpp::xstate::actor_system::subscribe

Adds a listener of the snapshots an actor settles in.

Synopsis

result<void>
subscribe(
    xactor::actor_ref actor,
    system_state::snapshot_listener listener);

Description

It ports XState’s actor.subscribe: the listener hears each snapshot the actor settles in while it is active, and the one it is done in, in order. Each call adds a listener, and an actor’s listeners hear in the order they were added. An empty listener is none.

Return Value

Success; webcpp::xactor::errc::invalid_argument for an address that names no actor, and from a callback.

Parameters

Name Description

actor

The actor.

listener

The listener.

See Also

"Watching an actor", in the guide.

"Guarantees", in the guide: guarantees A6 and A12.

webcpp::xstate::assign_action

An assign, which merges a partial update into the context.

Synopsis

struct assign_action;

Description

It ports XState’s assign(assigner). The update is merged into the context as Object.assign merges it: each member replaces the context’s member of that name, and the others stay. An assign changes the context and is never returned to the caller. The microstep fails with errc::implementation_failed when the context is not an object, or with the assigner’s own failure.

Data Members

Name Description

assignment

Computes the partial update.

See Also

"Assign", in the guide.

webcpp::xstate::assign_action::assignment

Computes the partial update.

Synopsis

assigner assignment = {};

webcpp::xstate::cancel_action

A cancel of a delayed raise or send.

Synopsis

struct cancel_action;

Description

It ports XState’s cancel(id), returned as xstate.cancel, its params holding sendId, the id of the delayed raise or send to cancel. A state with after returns one of its own when it is left.

Data Members

Name Description

id

The id of the delayed raise or send to cancel.

See Also

"Delayed raise, ids and cancel", in the guide.

webcpp::xstate::cancel_action::id

The id of the delayed raise or send to cancel.

Synopsis

std::string id = {};

webcpp::xstate::construct_actor

Constructs a machine child: it runs its initial macrostep and resolves its actions, then answers constructed.

Synopsis

struct construct_actor;

Description

It is XState’s createActor, which its parent’s resolveSpawn calls. The parent waits for the answer before it resolves its next action, so the children of the initial macrostep exist, and their systemIds are registered, when it does.

See Also

"Guarantees", in the guide: guarantee A5.

webcpp::xstate::constructed

A child’s answer to construct_actor, once its own children are constructed too.

Synopsis

struct constructed;

webcpp::xstate::delayed

A delayed event to the actor itself, armed under an id.

Synopsis

struct delayed;

Description

Its firing unregisters the id, as XState’s scheduler deletes its entry of timerMap, then the actor takes the event. A delayed event armed under no id comes back as the event itself.

Data Members

Name Description

event

The event the actor takes.

send_id

The id it was armed under, which its firing unregisters.

See Also

"Guarantees", in the guide: guarantee A7.

webcpp::xstate::delayed::event

The event the actor takes.

Synopsis

xstate::event event = {};

webcpp::xstate::delayed::send_id

The id it was armed under, which its firing unregisters.

Synopsis

std::string send_id = {};

webcpp::xstate::emit_action

An event emitted to the listeners of the actor, not to a machine.

Synopsis

struct emit_action;

Description

It ports XState’s emit(event). It is returned as xstate.emit, its params holding event, flat; the actor layer hands it to the inspector and to the listeners of actor_system::on_emitted.

Data Members

Name Description

event

Computes the event to emit.

See Also

"Emitting events", in the guide.

webcpp::xstate::emit_action::event

Computes the event to emit.

Synopsis

event_maker event = {};

webcpp::xstate::event

An event: its type, and the members that travel with it.

Synopsis

Declared in <webcpp/xstate/event.hpp>

struct event;

Description

It ports XState’s EventObject. XState writes an event flat, {"type": "SUBMIT", "user": "ana"}; xstate keeps the type apart, and the rest is the payload, {"user": "ana"}, which every implementation reads as args.event.payload. A caller builds one as xstate::event{.type = "SUBMIT", .payload = {{"user", "ana"}}}.

xstate makes some events itself: xstate.init, which runs the initial macrostep; xstate.done.state.<id>, raised when the state <id> reaches a final state, with the final state’s output when it has one; xstate.after.<delay>.<id>, the delayed event of a state’s after; and, in the actor layer, xstate.done.actor.<id> and xstate.error.actor.<id>, which a child sends its parent when it ends.

Data Members

Name Description

payload

Every other member of the event, in the order it was written.

type

The type, which the transitions’ event descriptors match.

See Also

"Events", in the guide.

webcpp::xstate::event::payload

Every other member of the event, in the order it was written.

Synopsis

Declared in <webcpp/xstate/event.hpp>

boost::json::object payload = {};

webcpp::xstate::event::type

The type, which the transitions’ event descriptors match.

Synopsis

Declared in <webcpp/xstate/event.hpp>

std::string type = {};

webcpp::xstate::forward_to_action

A send of the microstep’s event to another actor.

Synopsis

struct forward_to_action;

Description

It ports XState’s forwardTo(target, { id, delay }): a send_to_action of the microstep’s event, returned as xstate.sendTo with action::forwarded set. The microstep fails with errc::unknown_target for an empty target, as XState’s development build refuses every falsy one, and the actor layer fails the actor when a #system: target names no actor, where a sendTo goes to the actor itself.

Data Members

Name Description

delay

The delay, none for a send at once.

id

The id a cancel names a delayed send by.

target

The actor to forward to, as the target string names it.

See Also

"Sending to other actors", in the guide.

webcpp::xstate::forward_to_action::delay

The delay, none for a send at once.

Synopsis

std::optional<delay_ref> delay = {};

webcpp::xstate::forward_to_action::id

The id a cancel names a delayed send by.

Synopsis

std::optional<std::string> id = {};

webcpp::xstate::forward_to_action::target

The actor to forward to, as the target string names it.

Synopsis

std::string target = {};

webcpp::xstate::guard_node

A guard as the config names it: a registered guard, by name, with its params, or one of XState’s higher-order guards.

Synopsis

struct guard_node;

Description

XState’s higher-order guards are functions, which xstate’s JSON writes as {"type": "xstate.not", "guards": [g]}, xstate.and and xstate.or over a list of guards, and {"type": "xstate.stateIn", "stateValue": v}. and stops at its first false operand and or at its first true one. stateIn holds when the snapshot matches its state_value, or, for a string #id, when the node with that id is active.

Note

A machine keeps its guards in one flat list (machine::guard), and a higher-order guard names its operands by their index there, so a guard nested as deep as its JSON is held and evaluated without recursion.

Enums

Name Description

kind

What a guard is: a registered one, or one of the higher-order guards.

Data Members

Name Description

config

The guard as the config wrote it, for the definition.

kind

What this guard is.

operands

A higher-order guard’s operands, by their index in machine::guard.

params

A named guard’s params, null when the config gave none.

state_value

A stateIn guard’s state value, or its #id.

type

The guard’s type: its name, or xstate.not and the others.

See Also

"Higher-order guards and stateIn", in the guide.

"Higher-order guards", in the guide.

webcpp::xstate::guard_node::kind

What a guard is: a registered one, or one of the higher-order guards.

Synopsis

enum class kind : int;

Members

Name Description

named

A registered guard, by name.

negation

xstate.not, which holds when its one operand does not.

conjunction

xstate.and, which holds when every operand does.

disjunction

xstate.or, which holds when at least one operand does.

state_in

xstate.stateIn, which holds when the snapshot is in a state.

webcpp::xstate::guard_node::kind::named

A registered guard, by name.

Synopsis

named

webcpp::xstate::guard_node::kind::negation

xstate.not, which holds when its one operand does not.

Synopsis

negation

webcpp::xstate::guard_node::kind::conjunction

xstate.and, which holds when every operand does.

Synopsis

conjunction

webcpp::xstate::guard_node::kind::disjunction

xstate.or, which holds when at least one operand does.

Synopsis

disjunction

webcpp::xstate::guard_node::kind::state_in

xstate.stateIn, which holds when the snapshot is in a state.

Synopsis

state_in

webcpp::xstate::guard_node::config

The guard as the config wrote it, for the definition.

Synopsis

boost::json::value config = {};

webcpp::xstate::guard_node::kind

What this guard is.

Synopsis

kind kind = kind::named;

webcpp::xstate::guard_node::operands

A higher-order guard’s operands, by their index in machine::guard.

Synopsis

std::vector<std::size_t> operands = {};

webcpp::xstate::guard_node::params

A named guard’s params, null when the config gave none.

Synopsis

boost::json::value params = {};

webcpp::xstate::guard_node::state_value

A stateIn guard’s state value, or its #id.

Synopsis

boost::json::value state_value = {};

webcpp::xstate::guard_node::type

The guard’s type: its name, or xstate.not and the others.

Synopsis

std::string type = {};

webcpp::xstate::host_actor

An actor whose work is done outside the machines, by the host.

Synopsis

struct host_actor;

Description

It stands for XState’s fromPromise: invoking one asks the host, which later resolves it with an output or rejects it with an error, through actor_system::resolve and actor_system::reject. The machine core only names it; the actor layer runs it.

See Also

"Host actors", in the guide.

"Promises become host actors", in the guide.

webcpp::xstate::host_answer

The host’s answer to a host actor: its output, or its error.

Synopsis

struct host_answer;

Description

Data Members

Name Description

resolved

Whether the actor finishes done, with an output, rather than failed, with an error.

value

The output or the error; none for a promise settled with JavaScript’s undefined.

See Also

"Guarantees", in the guide: guarantee A10.

webcpp::xstate::host_answer::resolved

Whether the actor finishes done, with an output, rather than failed, with an error.

Synopsis

bool resolved = false;

webcpp::xstate::host_answer::value

The output or the error; none for a promise settled with JavaScript’s undefined.

Synopsis

std::optional<boost::json::value> value = {};

webcpp::xstate::host_logic

The logic of a host actor, in place of XState’s fromPromise.

Synopsis

class host_logic final
    : public xactor::actor_logic<actor_message>

Description

Once started, it is a pending host_request with its input. The host’s answer ends it: done with the answer as its output, its parent receiving xstate.done.actor.<id> with output and actorId; or failed with the answer as its error, its parent receiving xstate.error.actor.<id> with error and actorId, and its turn returning errc::actor_failed, so its xactor status is error. Answered, it releases its systemId before it reports. It ignores every other event, as XState’s promise actor does. machine_logic creates one when an invoke or a spawnChild names a host_actor; a host does not construct it.

Base Classes

Name Description

xactor::actor_logic<actor_message>

Type Aliases

Name Description

envelope_type

A message as the scheduler delivers it: a webcpp::xactor::envelope.

turn_type

What the actor may do while it handles a message: a webcpp::xactor::turn.

Member Functions

Name Description

host_logic [constructor]

Makes the logic of a host actor.

handle [virtual]

Handles one message of the actor’s mailbox.

See Also

"Host actors", in the guide.

"Promises become host actors", in the guide.

"Guarantees", in the guide: guarantee A10.

webcpp::xstate::host_logic::envelope_type

A message as the scheduler delivers it: a webcpp::xactor::envelope.

Synopsis

using envelope_type = xactor::envelope<actor_message>;

webcpp::xstate::host_logic::turn_type

What the actor may do while it handles a message: a webcpp::xactor::turn.

Synopsis

using turn_type = xactor::turn<actor_message>;

webcpp::xstate::host_logic::host_logic

Makes the logic of a host actor.

Synopsis

host_logic(
    system_state& state,
    std::optional<boost::json::value> input);

Parameters

Name Description

state

The state of the system the actor belongs to, which outlives it.

input

The actor’s input; none when it was given none, XState’s undefined.

webcpp::xstate::host_logic::handle

Handles one message of the actor’s mailbox.

Synopsis

virtual
result<void>
handle(
    turn_type& turn,
    envelope_type const& cause) override;

Description

A start_actor makes the actor a pending request, once. A host_answer to that request ends the actor, as the class says; one that comes before the start or after an answer changes nothing. A resume sends the report that the fuel of an earlier execution could not pay for. A stop_actor releases what the actor holds and stops it. Every other message is ignored.

Return Value

Success; errc::actor_failed when a rejection ends the actor, which its scheduler keeps for webcpp::xactor::scheduler::error_of; or the error of a send refused for another reason than fuel.

Parameters

Name Description

turn

What the actor may do while it handles the message.

cause

The message, in the envelope that says who sent it.

webcpp::xstate::host_request

A host actor’s request, pending until the host resolves or rejects it.

Synopsis

struct host_request;

Description

It is what XState’s fromPromise creator is called with. actor_system::host_requests lists the pending ones, and actor_system::resolve and actor_system::reject take one back.

Data Members

Name Description

actor

The host actor, which the answer ends.

id

The host actor’s id, its invoke’s or its spawnChild’s.

input

The host actor’s input; none when it was given none, XState’s undefined.

parent

The actor that invoked or spawned the host actor.

src

The name of the host actor in implementations::actors, its invoke’s or its spawnChild’s src.

See Also

"Host actors", in the guide.

"Guarantees", in the guide: guarantee A10.

webcpp::xstate::host_request::actor

The host actor, which the answer ends.

Synopsis

xactor::actor_ref actor = {};

webcpp::xstate::host_request::id

The host actor’s id, its invoke’s or its spawnChild’s.

Synopsis

std::string id = {};

webcpp::xstate::host_request::input

The host actor’s input; none when it was given none, XState’s undefined.

Synopsis

std::optional<boost::json::value> input = {};

webcpp::xstate::host_request::parent

The actor that invoked or spawned the host actor.

Synopsis

xactor::actor_ref parent = {};

webcpp::xstate::host_request::src

The name of the host actor in implementations::actors, its invoke’s or its spawnChild’s src.

Synopsis

std::string src = {};

webcpp::xstate::implementations

The implementations a machine is created with, each registered by the name its config gives it.

Synopsis

struct implementations;

Description

It ports XState’s setup({ actions, actors, guards, delays }). A JSON config cannot hold a function, so it names each implementation, and an implementations maps the names to C++ functions; create_machine takes it with the config. An empty one, {}, makes every action of the config a custom action, and holds no guard, delay or actor.

create_machine refuses, with errc::invalid_config, implementations that hold an empty std::function, which a program without exceptions could not call; an entry of inputs that names no invoke, or one whose config has an input; and an entry of outputs that names neither a final state nor the root, or one whose config has an output.

Data Members

Name Description

actions

The built-in actions, by action name; any other name is a custom action.

actors

What an invoke or a spawnChild runs, by its src.

context

Computes the initial context from the machine’s input, in place of the config’s context.

delays

The delays an after key or an action names, by delay name.

guards

The predicates a transition’s guard names, by guard name.

inputs

The input an invoke’s child is given, by invoke id, in place of the config’s input.

outputs

The output a final state is done with, by state id, or, by the root’s id, the machine’s, in place of the config’s output.

See Also

"Implementations", in the guide.

"Functions become implementations registered by name", in the guide.

webcpp::xstate::implementations::actions

The built-in actions, by action name; any other name is a custom action.

Synopsis

std::map<std::string, action_implementation, std::less<>> actions = {};

webcpp::xstate::implementations::actors

What an invoke or a spawnChild runs, by its src.

Synopsis

std::map<std::string, actor_implementation, std::less<>> actors = {};

webcpp::xstate::implementations::context

Computes the initial context from the machine’s input, in place of the config’s context.

Synopsis

std::optional<context_maker> context = {};

Description

It ports XState’s context: ({ input }) => .... A failure, or a value that is not an object (errc::implementation_failed), fails the initial macrostep.

webcpp::xstate::implementations::delays

The delays an after key or an action names, by delay name.

Synopsis

std::map<std::string, delay_maker, std::less<>> delays = {};

webcpp::xstate::implementations::guards

The predicates a transition’s guard names, by guard name.

Synopsis

std::map<std::string, predicate, std::less<>> guards = {};

webcpp::xstate::implementations::inputs

The input an invoke’s child is given, by invoke id, in place of the config’s input.

Synopsis

std::map<std::string, value_maker, std::less<>> inputs = {};

Description

It ports XState’s invoke input function, which JSON cannot hold. The input is computed when the invoking state is entered, after the state’s entry actions.

webcpp::xstate::implementations::outputs

The output a final state is done with, by state id, or, by the root’s id, the machine’s, in place of the config’s output.

Synopsis

std::map<std::string, value_maker, std::less<>> outputs = {};

Description

It ports XState’s output function. A final state’s output is computed from the context and the event that entered it; the machine’s, from the done event of the final state that completed it.

webcpp::xstate::inspector

What an inspector hears, after XState’s inspect.

Synopsis

struct inspector;

Description

It hears each actor’s start, each macrostep a machine actor settles, with the event that began it, and each event an actor emits. actor_system::inspect takes it, and an empty member is none. A macrostep that settles with the error errc::actor_failed, which an unhandled xstate.error.actor.<id> event gives, is heard with that snapshot.

Note

A macrostep that fails where XState throws is not heard, as XState inspects no snapshot for an actor whose transition threw. That is every failure of its cursor but errc::actor_failed (macrostep::next lists them): an implementation’s own failure; errc::implementation_failed from a built-in assign on a context that is not an object, or from a context function whose value is not one; errc::unknown_state for a value or a stateIn id that names no state; errc::invalid_event for an event of the type *; and errc::unknown_target for a sendTo the core finds naming no actor, or for the default of a history state that is the machine’s root. Nor is one that fails while its actions resolve (a systemId taken, a target that names no actor). One a deferred effect fails, a send bound to a child that is gone, is heard with the snapshot it reached, as XState’s update inspects it.

Data Members

Name Description

emitted

Hears an event an actor emits, before the actor’s own listeners.

settled

Hears a macrostep a machine actor settled: the actor, its machine, the event that began the macrostep and the snapshot it settled in.

started

Hears an actor’s start, with the actor’s address.

See Also

"Inspection", in the guide.

"Guarantees", in the guide: guarantees A9 and A12.

webcpp::xstate::inspector::emitted

Hears an event an actor emits, before the actor’s own listeners.

Synopsis

std::function<void(xactor::actor_ref, event const&)> emitted = {};

webcpp::xstate::inspector::settled

Hears a macrostep a machine actor settled: the actor, its machine, the event that began the macrostep and the snapshot it settled in.

Synopsis

std::function<void(xactor::actor_ref, machine const&, event const&, snapshot const&)> settled = {};

webcpp::xstate::inspector::started

Hears an actor’s start, with the actor’s address.

Synopsis

std::function<void(xactor::actor_ref)> started = {};

webcpp::xstate::invoke_definition

A child actor a state invokes, which lives while the state is active.

Synopsis

struct invoke_definition;

Description

It ports XState’s InvokeDefinition. Its onDone, onError and onSnapshot are the node’s transitions on xstate.done.actor.<id>, xstate.error.actor.<id> and xstate.snapshot.<id>. onSnapshot is accepted, but the actor layer sends no xstate.snapshot.<id>, since syncSnapshot and onSnapshot are not ported, so such a transition takes only an event its caller sends.

Note

The machine core runs no child: entering the state returns an xstate.spawnChild for it, after the state’s entry actions, and exiting it an xstate.stopChild, after its exit actions, which its caller executes.

Data Members

Name Description

config

The invoke as the config wrote it, for its transitions and the definition.

id

The config’s id, or XState’s default <index>.<state id>.

input

The config’s input, absent when it has none, XState’s undefined.

src

The name of an entry of implementations::actors.

system_id

The config’s systemId.

See Also

"Invoking actors", in the guide.

webcpp::xstate::invoke_definition::config

The invoke as the config wrote it, for its transitions and the definition.

Synopsis

boost::json::object config = {};

webcpp::xstate::invoke_definition::id

The config’s id, or XState’s default <index>.<state id>.

Synopsis

std::string id = {};

webcpp::xstate::invoke_definition::input

The config’s input, absent when it has none, XState’s undefined.

Synopsis

std::optional<boost::json::value> input = {};

webcpp::xstate::invoke_definition::src

The name of an entry of implementations::actors.

Synopsis

std::string src = {};

webcpp::xstate::invoke_definition::system_id

The config’s systemId.

Synopsis

std::optional<std::string> system_id = {};

webcpp::xstate::log_action

A log of a value, with an optional label.

Synopsis

struct log_action;

Description

It ports XState’s log(value, label). It is returned as xstate.log, its params holding value, what the value maker returned, and label when it has one. The actor layer hands it to the listener of actor_system::on_log.

Note

Without a value maker the value is an object of the context and the event, as XState logs {context, event}; a value maker that returns none, JavaScript’s undefined, leaves value out.

Data Members

Name Description

label

The label, written with the value.

value

Computes the value to log; without one, the context and the event.

See Also

"Log", in the guide.

webcpp::xstate::log_action::label

The label, written with the value.

Synopsis

std::optional<std::string> label = {};

webcpp::xstate::log_action::value

Computes the value to log; without one, the context and the event.

Synopsis

std::optional<value_maker> value = {};

webcpp::xstate::machine

The immutable tree of state nodes create_machine builds from a config, with the implementations it was given.

Synopsis

class machine;

Description

It ports XState’s StateMachine. Only create_machine makes one, so machine has no default constructor. It is a handle to data nothing changes once created: a copy is cheap and shares that data, and a cursor, an actor and a machine_actor each keep one.

Nodes are numbered in document order: the root 0, a parent before its children, and siblings in the order JavaScript enumerates the keys of states. The number is XState’s order unless two nodes share an id (state_node::order). Everything names a node by that index (snapshot::nodes, transition_definition::target), so the tree holds no pointer and copies as a value.

Member Functions

Name Description

child

Finds the child of a node by its key.

config

The config the machine was created from.

context

The initial context, unless implementations::context computes it.

guard

The guard at an index of the machine’s flat list of guards.

id

The machine’s id.

node

The node at an index.

node_by_id

Finds the node a state id names.

node_by_path

Finds the node a dotted path names, walking down from a node.

registry

The implementations the machine was created with.

root

The root node.

size

How many nodes the machine has.

Friends

Name Description

webcpp::xstate::create_machine

Builds a machine from the JSON of XState’s createMachine config, with the implementations its names stand for.

See Also

"Machines", in the guide.

"A machine is immutable", in the guide.

"State nodes and ids", in the guide.

webcpp::xstate::machine::child

Finds the child of a node by its key.

Synopsis

[[nodiscard]]
result<std::size_t>
child(
    std::size_t node,
    std::string_view key) const;

Description

A dot is part of the key, not a path.

Return Value

The child’s index; errc::unknown_state when node is not below size or has no such child.

Note

The return value should not be discarded.

Parameters

Name Description

node

The parent’s index.

key

The child’s key.

webcpp::xstate::machine::config

The config the machine was created from.

Synopsis

[[nodiscard]]
boost::json::object const&
config() const noexcept;

Return Value

The config, as given.

Note

The return value should not be discarded.

webcpp::xstate::machine::context

The initial context, unless implementations::context computes it.

Synopsis

[[nodiscard]]
boost::json::value const&
context() const noexcept;

Description

It is the config’s context, or {} when it has none or it is null, as XState starts a machine whose context is falsy with {}. Any other value is kept as written: a context of false, 0 or "", which XState also replaces with {}, stays what it is, and an assign on a context that is not an object fails with errc::implementation_failed, where XState’s Object.assign makes it an object, dropping true or a number and spreading a string or a list into numbered members.

Return Value

The initial context.

Note

The return value should not be discarded.

See Also

"Initial context", in the guide.

"A context that is not an object", in the guide.

webcpp::xstate::machine::guard

The guard at an index of the machine’s flat list of guards.

Synopsis

[[nodiscard]]
guard_node const&
guard(std::size_t index) const;

Description

A transition’s transition_definition::guard and a higher-order guard’s guard_node::operands index that list.

Return Value

The guard.

Note

The return value should not be discarded.

Parameters

Name Description

index

The guard’s index.

Preconditions

  • index is the index of a transition’s guard or of an operand.

webcpp::xstate::machine::id

The machine’s id.

Synopsis

[[nodiscard]]
std::string const&
id() const noexcept;

Return Value

The config’s id, or "(machine)" when it has none.

Note

The return value should not be discarded.

webcpp::xstate::machine::node

The node at an index.

Synopsis

[[nodiscard]]
state_node const&
node(std::size_t index) const;

Return Value

The node.

Note

The return value should not be discarded.

Parameters

Name Description

index

The node’s index, its position in document order.

Preconditions

  • index < size().

webcpp::xstate::machine::node_by_id

Finds the node a state id names.

Synopsis

[[nodiscard]]
result<std::size_t>
node_by_id(std::string_view state_id) const;

Description

It ports XState’s getStateNodeById. The leading # is optional. The first dot-separated segment is an id, and each segment after it walks down by key, so a default id such as (machine).a.a2 resolves through the root’s id.

Return Value

The node’s index; errc::unknown_state when the id or a key names no node.

Note

The return value should not be discarded.

Parameters

Name Description

state_id

The state id.

webcpp::xstate::machine::node_by_path

Finds the node a dotted path names, walking down from a node.

Synopsis

[[nodiscard]]
result<std::size_t>
node_by_path(
    std::size_t from,
    std::string_view state_path) const;

Description

It ports XState’s getStateNodeByPath. A path that starts with # names the node an id names, as node_by_id finds it.

Return Value

The node’s index; errc::unknown_state when from is not below size or the path names no node.

Note

The return value should not be discarded.

Parameters

Name Description

from

The index of the node the path starts from.

state_path

The dotted path, or a # id.

webcpp::xstate::machine::registry

The implementations the machine was created with.

Synopsis

[[nodiscard]]
implementations const&
registry() const noexcept;

Return Value

The implementations.

Note

The return value should not be discarded.

webcpp::xstate::machine::root

The root node.

Synopsis

[[nodiscard]]
state_node const&
root() const;

Return Value

node(0).

Note

The return value should not be discarded.

webcpp::xstate::machine::size

How many nodes the machine has.

Synopsis

[[nodiscard]]
std::size_t
size() const noexcept;

Return Value

The number of nodes, the root included.

Note

The return value should not be discarded.

webcpp::xstate::machine_actor

A machine run as a child actor, with that machine’s own implementations.

Synopsis

struct machine_actor;

Description

It is what XState’s setup({ actors }) takes for a machine. The machine core only names it; the actor layer runs it.

Note

A machine holds its implementations, which may hold this, so this holds the machine through a pointer; machine.hpp defines the constructor.

Member Functions

Name Description

machine_actor [constructor]

Makes the actor of a machine.

Data Members

Name Description

logic

The machine the child runs.

See Also

"Invoking actors", in the guide.

webcpp::xstate::machine_actor::machine_actor

Makes the actor of a machine.

Synopsis

explicit
machine_actor(machine run);

Parameters

Name Description

run

The machine the child runs, shared with every copy of this.

webcpp::xstate::machine_actor::logic

The machine the child runs.

Synopsis

std::shared_ptr<machine const> logic;

webcpp::xstate::machine_logic

The logic of a machine actor, XState’s createActor for a machine.

Synopsis

class machine_logic final
    : public xactor::actor_logic<actor_message>

Description

Each message is one turn. The actor keeps its snapshot and runs one macrostep at a time with the macrostep cursor, paying one unit of fuel a microstep. It resolves each microstep’s actions as the microstep is computed, and runs what they deferred (deliveries and timers, cancels, emits, the stops and starts of children) once the macrostep has settled. When its execution cannot pay, it parks with what it has not done, and only a resume continues it, so no event starts before the macrostep in progress has settled and no work runs unpaid or twice.

It ends done when its machine is done, after its parent has received its done event, and fails when a macrostep, or an effect it deferred, fails, after its parent has received its error event. That event, xstate.error.actor.<id>, carries as error the actor’s error_value, unchanged, when an unhandled error event failed it (errc::actor_failed), and no error when that event carried none. Otherwise it carries the name of the actor’s error code, its message(), such as "implementation_failed". Its turn then returns the failure, and its xactor status is error. actor_system::create_actor creates one, and so does a machine actor for each invoke or spawnChild that names a machine_actor; a host does not construct it.

Base Classes

Name Description

xactor::actor_logic<actor_message>

Type Aliases

Name Description

envelope_type

A message as the scheduler delivers it: a webcpp::xactor::envelope.

turn_type

What the actor may do while it handles a message: a webcpp::xactor::turn.

Member Functions

Name Description

machine_logic [constructor]

Makes the logic of a machine actor, which runs nothing until it is constructed or started.

handle [virtual]

Handles one message of the actor’s mailbox.

See Also

"Machine actors", in the guide.

"Done and error events", in the guide.

"Guarantees", in the guide: guarantees A2 to A6 and A9.

webcpp::xstate::machine_logic::envelope_type

A message as the scheduler delivers it: a webcpp::xactor::envelope.

Synopsis

using envelope_type = xactor::envelope<actor_message>;

webcpp::xstate::machine_logic::turn_type

What the actor may do while it handles a message: a webcpp::xactor::turn.

Synopsis

using turn_type = xactor::turn<actor_message>;

webcpp::xstate::machine_logic::machine_logic

Makes the logic of a machine actor, which runs nothing until it is constructed or started.

Synopsis

machine_logic(
    system_state& state,
    machine logic,
    boost::json::value input);

Parameters

Name Description

state

The state of the system the actor belongs to, which outlives it.

logic

The machine the actor runs.

input

The actor’s input, null for none.

webcpp::xstate::machine_logic::handle

Handles one message of the actor’s mailbox.

Synopsis

virtual
result<void>
handle(
    turn_type& turn,
    envelope_type const& cause) override;

Description

A construct_actor runs the initial macrostep and answers constructed; a start_actor starts the actor, constructing it first when no parent did; an event runs its macrostep, or waits while the actor has not started or has work left; a resume continues a parked actor; a relay forwards a delayed sendTo; a delayed is taken as its event; a stop_actor stops the actor once its work is done, and an event that arrives after it is dropped, as XState drops it; and a host_answer is ignored.

Return Value

Success, also when the actor parks for want of fuel; the failure that ends the actor, once its parent has received its error event; or the error of a call to turn refused for another reason than fuel.

Parameters

Name Description

turn

What the actor may do while it handles the message.

cause

The message, in the envelope that says who sent it.

webcpp::xstate::macrostep

The cursor of one macrostep, which runs one microstep a call.

Synopsis

class macrostep;

Description

It ports XState’s macrostep loop, one iteration a call. XState runs a macrostep to its end; the cursor lets its caller decide how much work an event may cost, one unit of fuel a call, and stop and resume where it likes. begin_initial and begin make one; it has no public constructor. A cursor is a value: it holds a copy of its machine, a handle, and of its snapshots and queue, so a copy resumes exactly where the original stopped, apart from it. xstate never stops a macrostep on its own: an always cycle never settles, and the caller’s fuel is the only bound.

Member Functions

Name Description

began_from

The snapshot the macrostep began from.

current

The snapshot as the last microstep left it.

done

Whether the macrostep has settled.

next

Runs exactly one microstep.

result

The settled macrostep.

Friends

Name Description

webcpp::xstate::begin

Makes the cursor of the macrostep an event runs from a snapshot.

webcpp::xstate::begin_initial

Makes the cursor of a machine’s initial macrostep.

See Also

"The macrostep cursor", in the guide.

"Fuel", in the guide.

"Guarantees", in the guide: guarantee 8.

webcpp::xstate::macrostep::began_from

The snapshot the macrostep began from.

Synopsis

[[nodiscard]]
snapshot const&
began_from() const noexcept;

Return Value

The snapshot before the macrostep’s first microstep.

Note

The return value should not be discarded.

webcpp::xstate::macrostep::current

The snapshot as the last microstep left it.

Synopsis

[[nodiscard]]
snapshot const&
current() const noexcept;

Return Value

The current snapshot, which between two microsteps is not a stable state.

Note

The return value should not be discarded.

webcpp::xstate::macrostep::done

Whether the macrostep has settled.

Synopsis

[[nodiscard]]
bool
done() const noexcept;

Return Value

true once it has; next then runs nothing.

Note

The return value should not be discarded.

webcpp::xstate::macrostep::next

Runs exactly one microstep.

Synopsis

progress
next();

Description

It runs the initial microstep, the external event’s, or one iteration of XState’s loop, an enabled eventless transition first and a raised event otherwise. Once the macrostep has settled it runs nothing, and returns a settled progress with the current snapshot, no list of transitions and no actions.

It returns no error: a failure settles the macrostep with the snapshot it began from, its status status::error and the failure as its error. The failure is an implementation’s, with its own code, or one of these:

  • errc::unknown_target when a sendTo names no actor, or a microstep needs the default of a history state that is the machine’s root;

  • errc::implementation_failed when an assign runs on a context that is not an object, for example the context of a config whose context is 5;

  • errc::invalid_event when the external event’s type is *; - errc::unknown_state when the snapshot’s value names no state of the machine, or a stateIn guard’s #id names none;

  • the context function’s error, or errc::implementation_failed when its value is not an object, on the first call of an initial macrostep.

When the failure comes from a guard of an eventless transition, which the cursor evaluates after a microstep to know whether it settled, the call reports the microstep it ran, with its transitions and actions, and the failed snapshot. An external event whose type starts with xstate.error.actor and that no transition takes is no failure: it settles in one microstep with an empty list of transitions, the snapshot it began from, its status status::error, its error errc::actor_failed and its error_value the event’s member error.

Note

XState’s actor, too, keeps the snapshot from before a macrostep that threw.

Return Value

What the call did.

See Also

"When an implementation fails", in the guide.

webcpp::xstate::macrostep::result

The settled macrostep.

Synopsis

[[nodiscard]]
macrostep_result const&
result() const noexcept;

Return Value

The result, meaningful once done.

Note

The return value should not be discarded.

webcpp::xstate::macrostep_result

A settled macrostep: its stable snapshot, every action its microsteps returned, and how many microsteps ran.

Synopsis

struct macrostep_result;

Description

When the macrostep fails, snapshot is the snapshot it began from, with the status status::error. actions still holds the actions of the microsteps that completed before the failure, including the microstep after which an eventless guard failed, and microsteps counts them. Of those actions, a running actor of XState has executed the custom actions and logs, which it runs as each microstep is resolved, and none of the built-in ones (a sendTo, an emit, a spawnChild, the stopChild of a started child, a delayed raise, a cancel), whose effects it defers until the macrostep has settled and drops when it throws. XState’s actor executes none of an initial macrostep that fails, since it runs an actor’s initial actions when the actor starts, and the start of an actor whose initial snapshot failed runs none. The actor layer does the same, and a caller that stands for XState’s actor executes the same ones.

Data Members

Name Description

actions

Every action the microsteps returned, in order.

microsteps

How many microsteps ran, a failing one excluded.

snapshot

The stable snapshot the macrostep settled in.

See Also

"When an implementation fails", in the guide.

webcpp::xstate::macrostep_result::actions

Every action the microsteps returned, in order.

Synopsis

std::vector<action> actions = {};

Description

When the macrostep settles not active, they are followed by an xstate.stopChild for each child the snapshot still lists, which no microstep returns (XState’s stopChildren). A macrostep that fails, or that an unhandled xstate.error.actor event settles, returns no such stop.

webcpp::xstate::macrostep_result::microsteps

How many microsteps ran, a failing one excluded.

Synopsis

std::size_t microsteps = 0;

webcpp::xstate::macrostep_result::snapshot

The stable snapshot the macrostep settled in.

Synopsis

xstate::snapshot snapshot = {};

webcpp::xstate::microstep

One microstep: the event it ran for, the transitions it took, the actions it returned and the snapshot it left.

Synopsis

struct microstep;

Description

It is what XState’s getMicrosteps and getInitialMicrosteps list for each microstep.

Data Members

Name Description

actions

The actions the microstep returned, in order.

event

The event the microstep ran for: the macrostep’s, a raised one, or the last one again for an eventless transition.

snapshot

The snapshot the microstep left.

transitions

The transitions the microstep took, as pointers into the machine.

See Also

"Microsteps", in the guide.

webcpp::xstate::microstep::actions

The actions the microstep returned, in order.

Synopsis

std::vector<action> actions = {};

webcpp::xstate::microstep::event

The event the microstep ran for: the macrostep’s, a raised one, or the last one again for an eventless transition.

Synopsis

xstate::event event = {};

webcpp::xstate::microstep::snapshot

The snapshot the microstep left.

Synopsis

xstate::snapshot snapshot = {};

webcpp::xstate::microstep::transitions

The transitions the microstep took, as pointers into the machine.

Synopsis

std::optional<std::vector<transition_definition const*>> transitions = {};

Description

It is none for the initial microstep, which XState does not report, and for a microstep that failed, and an empty list for an event no transition takes.

Note

A transition belongs to the machine and lives as long as any copy of it does.

webcpp::xstate::node_set

A set of node indices that keeps the order they were added in.

Synopsis

class node_set;

Description

It stands for the JavaScript Set in which XState keeps its active nodes, which enumerates its members in insertion order. The order is not cosmetic: it decides which eventless transition a parallel region takes first, and the order of the keys of a state value that are not array indices. xstate computes active nodes with it, and snapshot::nodes holds its ordered list.

Member Functions

Name Description

node_set [constructor]

Constructors

at

The node at a position of the insertion order.

begin

An iterator to the first node, in insertion order.

contains

Whether a node is in the set.

empty

Whether the set holds no node.

end

The end of the nodes, in insertion order.

erase

Removes a node, the others keeping their order.

insert

Adds a node at the end, unless it is already in the set.

ordered

The nodes, in insertion order.

size

How many nodes the set holds.

See Also

"The active state nodes", in the guide.

"Guarantees", in the guide: guarantee 10.

webcpp::xstate::node_set::node_set

Constructors

Synopses

Makes an empty set.

constexpr
node_set() = default;

Makes the set of the nodes of a range, in the range’s order.

template<class Range>
explicit
node_set(Range const& nodes);

Template Parameters

Name Description

Range

A range of std::size_t.

Parameters

Name Description

nodes

The nodes, each inserted as insert inserts it.

webcpp::xstate::node_set::node_set

Makes an empty set.

Synopsis

constexpr
node_set() = default;

webcpp::xstate::node_set::node_set

Makes the set of the nodes of a range, in the range’s order.

Synopsis

template<class Range>
explicit
node_set(Range const& nodes);

Template Parameters

Name Description

Range

A range of std::size_t.

Parameters

Name Description

nodes

The nodes, each inserted as insert inserts it.

webcpp::xstate::node_set::at

The node at a position of the insertion order.

Synopsis

[[nodiscard]]
std::size_t
at(std::size_t index) const;

Return Value

The node at index.

Note

The return value should not be discarded.

Parameters

Name Description

index

The position, from 0.

Preconditions

  • index < size().

webcpp::xstate::node_set::begin

An iterator to the first node, in insertion order.

Synopsis

[[nodiscard]]
auto
begin() const noexcept;

Return Value

An iterator over the nodes, as ordered lists them.

Note

The return value should not be discarded.

webcpp::xstate::node_set::contains

Whether a node is in the set.

Synopsis

[[nodiscard]]
bool
contains(std::size_t node) const noexcept;

Return Value

true when the set holds node.

Note

The return value should not be discarded.

Parameters

Name Description

node

The node’s index in its machine.

webcpp::xstate::node_set::empty

Whether the set holds no node.

Synopsis

[[nodiscard]]
bool
empty() const noexcept;

Return Value

true when size is 0.

Note

The return value should not be discarded.

webcpp::xstate::node_set::end

The end of the nodes, in insertion order.

Synopsis

[[nodiscard]]
auto
end() const noexcept;

Return Value

The iterator past the last node.

Note

The return value should not be discarded.

webcpp::xstate::node_set::erase

Removes a node, the others keeping their order.

Synopsis

void
erase(std::size_t node);

Description

A node not in the set is no error: nothing changes.

Parameters

Name Description

node

The node’s index in its machine.

webcpp::xstate::node_set::insert

Adds a node at the end, unless it is already in the set.

Synopsis

void
insert(std::size_t node);

Parameters

Name Description

node

The node’s index in its machine.

webcpp::xstate::node_set::ordered

The nodes, in insertion order.

Synopsis

[[nodiscard]]
std::vector<std::size_t> const&
ordered() const noexcept;

Return Value

The list of the nodes, valid until the set changes.

Note

The return value should not be discarded.

webcpp::xstate::node_set::size

How many nodes the set holds.

Synopsis

[[nodiscard]]
std::size_t
size() const noexcept;

Return Value

The number of nodes.

Note

The return value should not be discarded.

webcpp::xstate::progress

What one call of macrostep::next did: its microstep, and whether it settled the macrostep.

Synopsis

struct progress;

Description

A microstep that fails reports no actions, as XState’s pure functions throw; resolved_before_failure holds those it resolved before the failure, and the macrostep’s result holds none of them.

Data Members

Name Description

resolved_before_failure

The actions a microstep that failed resolved before the failure.

settled

Whether the microstep settled the macrostep.

step

The microstep the call ran.

See Also

"One microstep at a time", in the guide.

webcpp::xstate::progress::resolved_before_failure

The actions a microstep that failed resolved before the failure.

Synopsis

std::vector<action> resolved_before_failure = {};

Description

Of those, a running actor of XState has run the custom actions and logs, and none when the macrostep is an initial one. The actor layer reads it.

webcpp::xstate::progress::settled

Whether the microstep settled the macrostep.

Synopsis

bool settled = false;

Description

It is true on the microstep after which XState’s macrostep loop would stop, when no eventless transition is enabled and no raised event is queued, or the status is no longer status::active; and on no other.

See Also

"Guarantees", in the guide: guarantee 7.

webcpp::xstate::progress::step

The microstep the call ran.

Synopsis

microstep step = {};

webcpp::xstate::raise_action

A raise, which sends an event to the machine itself.

Synopsis

struct raise_action;

Description

It ports XState’s raise(event, { id, delay }). Without a delay, the event goes to the macrostep’s internal queue and runs in a later microstep of the same macrostep; with one, it is only returned, for the caller’s clock. Either way it is returned as xstate.raise, its params holding event, flat, id when it has one, and delay, in milliseconds, when it is delayed.

Data Members

Name Description

delay

The delay, none for a raise queued at once.

event

Computes the event to raise.

id

The id a cancel names a delayed raise by.

See Also

"Raise", in the guide.

"Delayed raise, ids and cancel", in the guide.

webcpp::xstate::raise_action::delay

The delay, none for a raise queued at once.

Synopsis

std::optional<delay_ref> delay = {};

webcpp::xstate::raise_action::event

Computes the event to raise.

Synopsis

event_maker event = {};

webcpp::xstate::raise_action::id

The id a cancel names a delayed raise by.

Synopsis

std::optional<std::string> id = {};

webcpp::xstate::relay

A delayed sendTo whose timer fired: its sender forwards event to to.

Synopsis

struct relay;

Description

Note

xactor releases a timer to the actor that armed it, as XState’s scheduler keys a delayed event by its source; the target was resolved when the sendTo was.

Data Members

Name Description

event

The event to forward.

send_id

The id it was armed under, which its firing unregisters; none when it had none.

to

The actor the sendTo’s target named when it was resolved.

See Also

"Guarantees", in the guide: guarantee A7.

webcpp::xstate::relay::event

The event to forward.

Synopsis

xstate::event event = {};

webcpp::xstate::relay::send_id

The id it was armed under, which its firing unregisters; none when it had none.

Synopsis

std::optional<std::string> send_id = {};

webcpp::xstate::relay::to

The actor the sendTo’s target named when it was resolved.

Synopsis

xactor::actor_ref to = {};

webcpp::xstate::resume

Continues an actor the fuel of an earlier execution could not finish.

Synopsis

struct resume;

Description

actor_system::resume sends it to each parked actor, in an execution of its own.

See Also

"Guarantees", in the guide: guarantee A3.

webcpp::xstate::send_parent_action

A send to the machine’s parent actor.

Synopsis

struct send_parent_action;

Description

It ports XState’s sendParent(event, { id, delay }): a send_to_action to #_parent, returned as xstate.sendTo to that target. It resolves for a machine without a parent too, where XState’s pure functions throw, their inert actor having none; the actor layer fails a root actor that sends to its parent, with errc::unknown_target.

Data Members

Name Description

delay

The delay, none for a send at once.

event

Computes the event to send.

id

The id a cancel names a delayed send by.

See Also

"Sending to other actors", in the guide.

webcpp::xstate::send_parent_action::delay

The delay, none for a send at once.

Synopsis

std::optional<delay_ref> delay = {};

webcpp::xstate::send_parent_action::event

Computes the event to send.

Synopsis

event_maker event = {};

webcpp::xstate::send_parent_action::id

The id a cancel names a delayed send by.

Synopsis

std::optional<std::string> id = {};

webcpp::xstate::send_to_action

A send of an event to another actor, named by a target string.

Synopsis

struct send_to_action;

Description

It ports XState’s sendTo(target, event, { id, delay }). The target is a string as XState reads one: a child’s id, or #_ followed by it; #_parent, the parent; #_internal, the actor itself; or #system: followed by a systemId, the actor registered under it. It is returned as xstate.sendTo, its params holding targetId, event, flat, and id and delay as a raise’s. The machine core delivers nothing: its caller does, or the actor layer.

The microstep fails with errc::unknown_target when the target names a child the snapshot does not hold, unless an entry or initial action names, by its bare id, an invoke of the state being entered: that sendTo carries action::bound_at. #_parent, #_internal and #system: targets are always accepted here; the actor layer resolves them.

Note

XState sends to a systemId through a function of the system, ({ system }) => system.get(id), which JSON cannot hold.

Data Members

Name Description

delay

The delay, none for a send at once.

event

Computes the event to send.

id

The id a cancel names a delayed send by.

target

The actor to send to, as the target string names it.

See Also

"Sending to other actors", in the guide.

webcpp::xstate::send_to_action::delay

The delay, none for a send at once.

Synopsis

std::optional<delay_ref> delay = {};

webcpp::xstate::send_to_action::event

Computes the event to send.

Synopsis

event_maker event = {};

webcpp::xstate::send_to_action::id

The id a cancel names a delayed send by.

Synopsis

std::optional<std::string> id = {};

webcpp::xstate::send_to_action::target

The actor to send to, as the target string names it.

Synopsis

std::string target = {};

webcpp::xstate::simulated_clock

A clock the caller owns, which holds the delayed events of the machine core until the time it is set to reaches them.

Synopsis

Declared in <webcpp/xstate/clock.hpp>

class simulated_clock;

Description

It ports XState’s SimulatedClock with its scheduler. The machine core never reads a clock: a delayed event comes back as an action, and a clock holds it until the time the caller sets reaches it. Time is in whole milliseconds, where XState’s takes fractions, and starts at 0. The actor layer keeps its timers in xactor instead.

Member Functions

Name Description

apply

Applies to the clock a built-in action a step returned.

cancel

Forgets the event scheduled under an id, if the id still names one.

increment

Moves the time forward.

now

The time.

pop_due

Takes the earliest event whose time has come out of the clock.

schedule

Holds an event until a delay from now.

set

Sets the time, which never goes back.

See Also

"Delayed events and the simulated clock", in the guide.

"In the machine core", in the guide.

"Time in whole milliseconds", in the guide.

webcpp::xstate::simulated_clock::apply

Applies to the clock a built-in action a step returned.

Synopsis

Declared in <webcpp/xstate/clock.hpp>

void
apply(action const& returned);

Description

A built-in xstate.raise with a delay is scheduled, under its id when it has one, and a built-in xstate.cancel cancels its sendId. A delayed built-in xstate.sendTo with an id, a sendParent’s included, is not the machine’s to receive, but holds its id until it comes due, as XState’s scheduler keys every delayed event by its id. A raise without a delay, which its macrostep already ran, and every other action are ignored.

Note

A custom action is ignored whatever its type, a config being free to name one xstate.raise: XState’s scheduler, too, holds only what its built-in actions schedule.

Parameters

Name Description

returned

An action a step returned.

webcpp::xstate::simulated_clock::cancel

Forgets the event scheduled under an id, if the id still names one.

Synopsis

Declared in <webcpp/xstate/clock.hpp>

void
cancel(std::string_view id);

Description

It ports XState’s scheduler.cancel.

Parameters

Name Description

id

The id.

webcpp::xstate::simulated_clock::increment

Moves the time forward.

Synopsis

Declared in <webcpp/xstate/clock.hpp>

void
increment(uint64_t milliseconds) noexcept;

Description

It ports XState’s SimulatedClock.increment.

Parameters

Name Description

milliseconds

How far.

webcpp::xstate::simulated_clock::now

The time.

Synopsis

Declared in <webcpp/xstate/clock.hpp>

[[nodiscard]]
uint64_t
now() const noexcept;

Return Value

The time, in milliseconds since the clock was made.

Note

The return value should not be discarded.

webcpp::xstate::simulated_clock::pop_due

Takes the earliest event whose time has come out of the clock.

Synopsis

Declared in <webcpp/xstate/clock.hpp>

std::optional<event>
pop_due();

Description

The earliest is by deadline, then by the order the events were scheduled in, a rule where XState leaves equal deadlines to its sort. The id it was scheduled under then cancels nothing, as XState’s firing timeout deletes its entry of timerMap. The caller delivers each due event as a macrostep of its own and applies what that returns before the next call, so an event it schedules or cancels is ordered as XState orders it.

Return Value

The event; none when no event is due.

See Also

"Equal deadlines", in the guide.

webcpp::xstate::simulated_clock::schedule

Holds an event until a delay from now.

Synopsis

Declared in <webcpp/xstate/clock.hpp>

void
schedule(
    event delayed,
    uint64_t delay,
    std::optional<std::string> id);

Description

A later schedule under the same id takes the id over: the earlier event still comes due, but a cancel of the id no longer finds it, as in XState’s scheduler.

Parameters

Name Description

delayed

The event.

delay

The delay, in milliseconds from now.

id

The id a cancel names the event by, or none.

webcpp::xstate::simulated_clock::set

Sets the time, which never goes back.

Synopsis

Declared in <webcpp/xstate/clock.hpp>

result<void>
set(uint64_t time);

Description

It ports XState’s SimulatedClock.set, which refuses to go back in time.

Return Value

Success; errc::invalid_config when time is earlier than now, which changes nothing.

Parameters

Name Description

time

The time, in milliseconds.

webcpp::xstate::snapshot

Where a machine is, as a plain value to keep, copy or store.

Synopsis

struct snapshot;

Description

It ports XState’s machine snapshot. xstate makes snapshots and keeps none: a caller keeps the one a macrostep settled in and runs the next event from it. A default-constructed snapshot is no machine’s; start from get_initial_snapshot or a cursor.

Member Functions

Name Description

has_tag

Whether an active node carries a tag.

matches

Whether the snapshot is in a state value.

remembered

What a history node remembers.

Data Members

Name Description

children

The child actors, (id, src), that the microsteps spawned, for an invoke or a spawn_child_action, and have not stopped since.

context

The context.

error

Why the status is status::error; an empty code otherwise.

error_value

With the error errc::actor_failed, the failed actor’s error, when its error event carried one.

history

What each history node remembers, by its id, in recording order.

nodes

The active nodes, by index, in XState’s order.

output

The machine’s output once it is done; absent when it has none, JavaScript’s undefined.

status

Whether the machine runs, is done, or failed.

tags

The tags of the active nodes, each once, in the order of nodes.

value

XState’s state value, computed from nodes.

See Also

"The snapshot", in the guide.

"A snapshot is a value", in the guide.

"Persisted state", in the guide.

webcpp::xstate::snapshot::has_tag

Whether an active node carries a tag.

Synopsis

[[nodiscard]]
bool
has_tag(std::string_view tag) const;

Description

It ports XState’s snapshot.hasTag.

Return Value

true when tags holds tag.

Note

The return value should not be discarded.

Parameters

Name Description

tag

The tag.

webcpp::xstate::snapshot::matches

Whether the snapshot is in a state value.

Synopsis

[[nodiscard]]
bool
matches(boost::json::value const& state_value) const;

Description

It ports XState’s snapshot.matches: matches_state with the snapshot’s value as the child.

Return Value

true when the snapshot is in state_value or a state within it.

Note

The return value should not be discarded.

Parameters

Name Description

state_value

A state value, or a dotted string.

webcpp::xstate::snapshot::remembered

What a history node remembers.

Synopsis

[[nodiscard]]
std::vector<std::size_t> const*
remembered(std::string_view history_id) const;

Return Value

A pointer into history, or nullptr when the node has not recorded.

Note

The return value should not be discarded.

Parameters

Name Description

history_id

The history node’s id.

webcpp::xstate::snapshot::children

The child actors, (id, src), that the microsteps spawned, for an invoke or a spawn_child_action, and have not stopped since.

Synopsis

std::vector<std::pair<std::string, std::string>> children = {};

Description

They are listed as JavaScript enumerates XState’s children object: ids that are array indices first, by number, then the others in spawning order.

See Also

"Guarantees", in the guide: guarantee 18.

webcpp::xstate::snapshot::context

The context.

Synopsis

boost::json::value context = {};

webcpp::xstate::snapshot::error

Why the status is status::error; an empty code otherwise.

Synopsis

boost::system::error_code error = {};

webcpp::xstate::snapshot::error_value

With the error errc::actor_failed, the failed actor’s error, when its error event carried one.

Synopsis

std::optional<boost::json::value> error_value = {};

webcpp::xstate::snapshot::history

What each history node remembers, by its id, in recording order.

Synopsis

std::vector<std::pair<std::string, std::vector<std::size_t>>> history = {};

webcpp::xstate::snapshot::nodes

The active nodes, by index, in XState’s order.

Synopsis

std::vector<std::size_t> nodes = {};

Description

A node kept active keeps its place, an exited one leaves, and entered ones are appended in document order. The order decides which eventless transition a parallel region takes first, and the order of the keys of the state value that are not array indices. A microstep that leaves the machine done lists the nodes by descending order, each after its descendants, because XState sorts its array of active nodes in place when the machine is done. resolve_state sorts nothing, done or not, as XState’s resolveState does not.

See Also

"Guarantees", in the guide: guarantee 10.

webcpp::xstate::snapshot::output

The machine’s output once it is done; absent when it has none, JavaScript’s undefined.

Synopsis

std::optional<boost::json::value> output = {};

webcpp::xstate::snapshot::status

Whether the machine runs, is done, or failed.

Synopsis

status status = status::active;

webcpp::xstate::snapshot::tags

The tags of the active nodes, each once, in the order of nodes.

Synopsis

std::vector<std::string> tags = {};

webcpp::xstate::snapshot::value

XState’s state value, computed from nodes.

Synopsis

boost::json::value value = {};

Description

The keys of each of its objects are in the order JavaScript enumerates them in XState’s value: those that are array indices first, by number, then the others in the order of nodes.

webcpp::xstate::spawn_child_action

A spawn of a child actor that lives until it is stopped or the machine is done.

Synopsis

struct spawn_child_action;

Description

It ports XState’s spawnChild(src, { id, systemId, input }). Resolving it adds the child, (id, src), to the snapshot’s children, a child already there under that id taking the new src, and returns xstate.spawnChild, its params holding id, src, and input and systemId when there are any. The machine core runs no child: its caller does, or the actor layer. create_machine refuses, with errc::unknown_actor, a spawnChild the config names whose src is not an actor of the implementations.

Note

XState keys a child spawned without an id "undefined", and a stopChild then leaves it there; an id left out here is "", and the child is keyed under it.

Data Members

Name Description

id

The child’s id: fixed, or computed when the action resolves.

input

Computes the child’s input; without one the input is undefined.

src

The name of an entry of implementations::actors.

system_id

The systemId the child is registered under in its actor system.

See Also

"Spawning actors", in the guide.

"A spawned child without an id", in the guide.

webcpp::xstate::spawn_child_action::id

The child’s id: fixed, or computed when the action resolves.

Synopsis

std::variant<std::string, id_maker> id = {};

webcpp::xstate::spawn_child_action::input

Computes the child’s input; without one the input is undefined.

Synopsis

std::optional<value_maker> input = {};

webcpp::xstate::spawn_child_action::src

The name of an entry of implementations::actors.

Synopsis

std::string src = {};

webcpp::xstate::spawn_child_action::system_id

The systemId the child is registered under in its actor system.

Synopsis

std::optional<std::string> system_id = {};

webcpp::xstate::start_actor

Starts the actor, constructing it first if no parent did.

Synopsis

struct start_actor;

Description

The actor hands over what its initial macrostep returned and runs what it deferred, the starts of its active children first: XState’s actor.start(). actor_system::start sends it to a root, and a parent to each child it created once its macrostep has settled.

See Also

"Guarantees", in the guide: guarantee A4.

webcpp::xstate::state_node

One node of a machine.

Synopsis

struct state_node;

Description

It ports what XState’s StateNode holds once initialised. A program reads it through machine::node; nothing here is built by hand.

Data Members

Name Description

always

Its eventless transitions.

config

Its config, without its states.

description

The config’s description.

entry

Its entry actions, with the delayed raises an after adds.

exit

Its exit actions, with the cancels an after adds.

history

For a history node, its kind.

history_target

A history node’s default target, resolved.

id

Its id: the config’s id, or the machine’s id followed by the path, dot-separated.

initial

Its initial transition, which every node has.

invoke

The children it invokes.

key

Its key in its parent’s states, the root’s being the machine’s id.

meta

The config’s meta.

order

XState’s order: the number of distinct ids registered before this node.

output

The config’s output, kept only for a final state or the root.

parent

Its parent, none for the root.

path

The keys from the root down to it, the root’s excluded.

states

Its children, by key, in the order JavaScript enumerates the config’s keys.

tags

The config’s tags.

transitions

Its transitions by event descriptor, in XState’s Map order.

type

Its type.

See Also

"State nodes and ids", in the guide.

webcpp::xstate::state_node::always

Its eventless transitions.

Synopsis

std::vector<transition_definition> always = {};

webcpp::xstate::state_node::config

Its config, without its states.

Synopsis

boost::json::object config = {};

webcpp::xstate::state_node::description

The config’s description.

Synopsis

std::optional<std::string> description = {};

Description

A description that is not a string is none, where XState keeps it.

webcpp::xstate::state_node::entry

Its entry actions, with the delayed raises an after adds.

Synopsis

std::vector<action_ref> entry = {};

webcpp::xstate::state_node::exit

Its exit actions, with the cancels an after adds.

Synopsis

std::vector<action_ref> exit = {};

webcpp::xstate::state_node::history

For a history node, its kind.

Synopsis

history_kind history = history_kind::none;

webcpp::xstate::state_node::history_target

A history node’s default target, resolved.

Synopsis

std::optional<std::vector<std::size_t>> history_target = {};

Description

It is none when the config names none. It is also none for a history node that is the machine’s root, which has no parent to resolve the target against, except that the target [] gives an empty list.

webcpp::xstate::state_node::id

Its id: the config’s id, or the machine’s id followed by the path, dot-separated.

Synopsis

std::string id = {};

webcpp::xstate::state_node::initial

Its initial transition, which every node has.

Synopsis

transition_definition initial = {};

Description

It goes to the child its initial names, with the actions an object initial gives. A node without initial has an empty target; a compound node must have one.

webcpp::xstate::state_node::invoke

The children it invokes.

Synopsis

std::vector<invoke_definition> invoke = {};

webcpp::xstate::state_node::key

Its key in its parent’s states, the root’s being the machine’s id.

Synopsis

std::string key = {};

webcpp::xstate::state_node::meta

The config’s meta.

Synopsis

std::optional<boost::json::value> meta = {};

Description

A null meta is none, where XState keeps it.

webcpp::xstate::state_node::order

XState’s order: the number of distinct ids registered before this node.

Synopsis

std::size_t order = 0;

Description

Unless two nodes share an id, it is the node’s position in document order.

webcpp::xstate::state_node::output

The config’s output, kept only for a final state or the root.

Synopsis

std::optional<boost::json::value> output = {};

webcpp::xstate::state_node::parent

Its parent, none for the root.

Synopsis

std::optional<std::size_t> parent = {};

webcpp::xstate::state_node::path

The keys from the root down to it, the root’s excluded.

Synopsis

std::vector<std::string> path = {};

webcpp::xstate::state_node::states

Its children, by key, in the order JavaScript enumerates the config’s keys.

Synopsis

std::vector<std::pair<std::string, std::size_t>> states = {};

webcpp::xstate::state_node::tags

The config’s tags.

Synopsis

std::vector<std::string> tags = {};

webcpp::xstate::state_node::transitions

Its transitions by event descriptor, in XState’s Map order.

Synopsis

std::vector<std::pair<std::string, std::vector<transition_definition>>> transitions = {};

Description

on comes first, then onDone under xstate.done.state.<id>, then each invoke’s onDone, onError and onSnapshot, then the delayed transitions. The keys of on and of after are taken in the order JavaScript enumerates them. A descriptor given twice keeps its first place and its last transitions, except a delay, whose transitions append.

webcpp::xstate::state_node::type

Its type.

Synopsis

node_type type = node_type::atomic;

webcpp::xstate::stop_actor

Asks an actor to stop with its family, once it has handled what its parent sent it before.

Synopsis

struct stop_actor;

Description

It is XState’s deferred stopChild, which runs after the sends of the same macrostep. The actor finishes the macrostep it runs and the events that arrived before, ignores what arrives after, asks each of its own children to stop the same way, and stops, running no exit action. A child that has already ended ignores it.

See Also

"Stopping a child", in the guide.

"Guarantees", in the guide: guarantee A6.

webcpp::xstate::stop_child_action

A stop of a child actor.

Synopsis

struct stop_child_action;

Description

It ports XState’s stopChild(id). It removes the child id from the snapshot’s children and is returned as xstate.stopChild, its params holding id, or null when the snapshot held no such child. The actor layer stops the child with its family; a stop runs no exit action.

Data Members

Name Description

id

The id of the child to stop.

See Also

"Stopping a child", in the guide.

webcpp::xstate::stop_child_action::id

The id of the child to stop.

Synopsis

std::string id = {};

webcpp::xstate::system_state

What an actor system knows beside xactor’s statuses, shared by every actor of the system.

Synopsis

class system_state;

Description

It holds each actor’s record, the registry of systemIds, the host’s pending requests, the parked actors and the host’s callbacks. An actor_system owns one, and its actors reach it through the reference their logic was built with. A host does not call it: its listener types name the callbacks actor_system takes, and its members serve machine_logic and host_logic.

Note

An actor reaches it only during its own turn, so the system’s single thread is the only one that ever touches it.

Type Aliases

Name Description

action_listener

The listener of every custom action, with the actor that returned it, which actor_system::on_action sets.

event_listener

A listener of an actor’s emitted events, which actor_system::on_emitted adds.

log_listener

The listener of every log, with the actor that returned it, which actor_system::on_log sets.

snapshot_listener

A listener of an actor’s snapshots, which actor_system::subscribe adds.

Member Functions

Name Description

act

Hands a custom action to the host’s action listener.

add

Records an actor, the host’s or a spawned one, and its systemId.

adopt

Records a child spawned by parent; a child already there under the same id is replaced in the parent’s children, as XState’s snapshot replaces it, and keeps running.

answered

Removes the pending request of a host actor the host has answered.

ask

Adds a host actor’s pending request.

book

Counts an actor created, or one whose creation XState begins and a claim of its systemId then refuses; XState’s system._bookId.

booked

How many actors the system created or began to create.

child_of

The child an actor knows by an id, while it has it.

emit

Hands an emitted event to the inspector, then to the actor’s listeners, in listening order.

family_of

An actor and every actor descended from it.

forget_child

Removes the child an actor knows by an id from the actor’s children.

inspect

Sets the inspector, replacing the previous one.

listen

Keeps a listener of an actor’s emitted events; an empty one is none.

log

Hands a log to the host’s log listener.

notify

Hands a settled snapshot to the actor’s subscribers, in subscribing order.

on_action

Sets the listener of every custom action, replacing the previous one.

on_log

Sets the listener of every log, replacing the previous one.

park

Counts an actor among the parked, which wait for a resume.

parked

The parked actors.

publish

Keeps the snapshot a machine actor settled in, which actor_system::snapshot_of returns.

reattach

Gives a parent back a child under an id, unless another child holds that id.

record_of

What the system keeps of an actor.

registered

The actor registered under a systemId, while it holds it.

release

Lets go of what an actor that has ended held: its systemId, its pending request and its place among the parked.

requests

The pending requests of the host actors.

settled

Tells the inspector a machine actor settled a macrostep.

started

Tells the inspector an actor started.

subscribe

Keeps a listener of an actor’s snapshots; an empty one is none, as an empty callback is.

unpark

Takes an actor out of the parked.

unregister

Releases the systemId an actor holds, and only its; XState’s system._unregister.

unregister_family

Releases the systemIds an actor and its descendants hold at once, as XState’s stopChild does before the stop itself runs.

See Also

"How the layer runs", in the guide.

webcpp::xstate::system_state::action_listener

The listener of every custom action, with the actor that returned it, which actor_system::on_action sets.

Synopsis

using action_listener = std::function<void(xactor::actor_ref, action const&)>;

webcpp::xstate::system_state::event_listener

A listener of an actor’s emitted events, which actor_system::on_emitted adds.

Synopsis

using event_listener = std::function<void(event const&)>;

webcpp::xstate::system_state::log_listener

The listener of every log, with the actor that returned it, which actor_system::on_log sets.

Synopsis

using log_listener = std::function<void(xactor::actor_ref, boost::json::value const*, std::string_view)>;

Description

The value is nullptr for JavaScript’s undefined, and lives during the call; the label is empty when the log has none.

webcpp::xstate::system_state::snapshot_listener

A listener of an actor’s snapshots, which actor_system::subscribe adds.

Synopsis

using snapshot_listener = std::function<void(snapshot const&)>;

webcpp::xstate::system_state::act

Hands a custom action to the host’s action listener.

Synopsis

void
act(
    xactor::actor_ref actor,
    action const& custom) const;

Parameters

Name Description

actor

The actor that returned it.

custom

The action.

webcpp::xstate::system_state::add

Records an actor, the host’s or a spawned one, and its systemId.

Synopsis

void
add(
    xactor::actor_ref actor,
    actor_record record);

Description

It counts the actor, as book does, and registers the actor under its systemId, if it has one.

Parameters

Name Description

actor

The actor.

record

What to keep of it.

webcpp::xstate::system_state::adopt

Records a child spawned by parent; a child already there under the same id is replaced in the parent’s children, as XState’s snapshot replaces it, and keeps running.

Synopsis

void
adopt(
    xactor::actor_ref parent,
    xactor::actor_ref child,
    actor_record record);

Parameters

Name Description

parent

The actor that spawned the child.

child

The child.

record

What to keep of the child, whose parent becomes parent.

webcpp::xstate::system_state::answered

Removes the pending request of a host actor the host has answered.

Synopsis

void
answered(xactor::actor_ref actor);

Parameters

Name Description

actor

The host actor.

webcpp::xstate::system_state::ask

Adds a host actor’s pending request.

Synopsis

void
ask(host_request request);

Parameters

Name Description

request

The request.

webcpp::xstate::system_state::book

Counts an actor created, or one whose creation XState begins and a claim of its systemId then refuses; XState’s system._bookId.

Synopsis

void
book() noexcept;

webcpp::xstate::system_state::booked

How many actors the system created or began to create.

Synopsis

[[nodiscard]]
uint64_t
booked() const noexcept;

Return Value

The count, which names a root created without an id.

Note

The return value should not be discarded.

webcpp::xstate::system_state::child_of

The child an actor knows by an id, while it has it.

Synopsis

[[nodiscard]]
std::optional<xactor::actor_ref>
child_of(
    xactor::actor_ref parent,
    std::string_view id) const;

Return Value

The child; none when parent has no child of that id.

Note

The return value should not be discarded.

Parameters

Name Description

parent

The actor.

id

The child’s id.

webcpp::xstate::system_state::emit

Hands an emitted event to the inspector, then to the actor’s listeners, in listening order.

Synopsis

void
emit(
    xactor::actor_ref actor,
    event const& emitted) const;

Parameters

Name Description

actor

The actor that emitted it.

emitted

The event.

webcpp::xstate::system_state::family_of

An actor and every actor descended from it.

Synopsis

[[nodiscard]]
std::set<uint32_t>
family_of(xactor::actor_ref actor) const;

Return Value

The values of their addresses.

Note

The return value should not be discarded.

Parameters

Name Description

actor

The actor.

webcpp::xstate::system_state::forget_child

Removes the child an actor knows by an id from the actor’s children.

Synopsis

void
forget_child(
    xactor::actor_ref parent,
    std::string_view id);

Parameters

Name Description

parent

The actor.

id

The child’s id.

webcpp::xstate::system_state::inspect

Sets the inspector, replacing the previous one.

Synopsis

void
inspect(inspector heard);

Parameters

Name Description

heard

The inspector.

webcpp::xstate::system_state::listen

Keeps a listener of an actor’s emitted events; an empty one is none.

Synopsis

void
listen(
    xactor::actor_ref actor,
    event_listener listener);

Parameters

Name Description

actor

The actor.

listener

The listener.

webcpp::xstate::system_state::log

Hands a log to the host’s log listener.

Synopsis

void
log(
    xactor::actor_ref actor,
    boost::json::value const* value,
    std::string_view label) const;

Parameters

Name Description

actor

The actor that returned it.

value

The logged value, nullptr for JavaScript’s undefined.

label

The log’s label, empty when it has none.

webcpp::xstate::system_state::notify

Hands a settled snapshot to the actor’s subscribers, in subscribing order.

Synopsis

void
notify(
    xactor::actor_ref actor,
    snapshot const& settled) const;

Parameters

Name Description

actor

The actor.

settled

The snapshot.

webcpp::xstate::system_state::on_action

Sets the listener of every custom action, replacing the previous one.

Synopsis

void
on_action(action_listener listener);

Parameters

Name Description

listener

The listener; an empty one is none.

webcpp::xstate::system_state::on_log

Sets the listener of every log, replacing the previous one.

Synopsis

void
on_log(log_listener listener);

Parameters

Name Description

listener

The listener; an empty one is none.

webcpp::xstate::system_state::park

Counts an actor among the parked, which wait for a resume.

Synopsis

void
park(xactor::actor_ref actor);

Parameters

Name Description

actor

The actor.

webcpp::xstate::system_state::parked

The parked actors.

Synopsis

[[nodiscard]]
std::set<uint32_t> const&
parked() const noexcept;

Return Value

The values of their addresses.

Note

The return value should not be discarded.

webcpp::xstate::system_state::publish

Keeps the snapshot a machine actor settled in, which actor_system::snapshot_of returns.

Synopsis

void
publish(
    xactor::actor_ref actor,
    snapshot const& settled);

Parameters

Name Description

actor

The actor.

settled

The snapshot.

webcpp::xstate::system_state::reattach

Gives a parent back a child under an id, unless another child holds that id.

Synopsis

void
reattach(
    xactor::actor_ref parent,
    std::string_view id,
    xactor::actor_ref child);

Parameters

Name Description

parent

The parent.

id

The id.

child

The child.

webcpp::xstate::system_state::record_of

What the system keeps of an actor.

Synopsis

[[nodiscard]]
actor_record const*
record_of(xactor::actor_ref actor) const;

Return Value

The record, valid while the system lives; nullptr for an address the system has not recorded.

Note

The return value should not be discarded.

Parameters

Name Description

actor

The actor.

webcpp::xstate::system_state::registered

The actor registered under a systemId, while it holds it.

Synopsis

[[nodiscard]]
std::optional<xactor::actor_ref>
registered(std::string_view system_id) const;

Return Value

The actor; none when no actor holds system_id.

Note

The return value should not be discarded.

Parameters

Name Description

system_id

The systemId.

See Also

"Guarantees", in the guide: guarantee A8.

webcpp::xstate::system_state::release

Lets go of what an actor that has ended held: its systemId, its pending request and its place among the parked.

Synopsis

void
release(xactor::actor_ref actor);

Parameters

Name Description

actor

The actor.

webcpp::xstate::system_state::requests

The pending requests of the host actors.

Synopsis

[[nodiscard]]
std::vector<host_request> const&
requests() const noexcept;

Return Value

The requests, in the order they were made.

Note

The return value should not be discarded.

webcpp::xstate::system_state::settled

Tells the inspector a machine actor settled a macrostep.

Synopsis

void
settled(
    xactor::actor_ref actor,
    machine const& logic,
    event const& cause,
    snapshot const& settled) const;

Parameters

Name Description

actor

The actor.

logic

Its machine.

cause

The event that began the macrostep.

settled

The snapshot the macrostep settled in.

webcpp::xstate::system_state::started

Tells the inspector an actor started.

Synopsis

void
started(xactor::actor_ref actor) const;

Parameters

Name Description

actor

The actor.

webcpp::xstate::system_state::subscribe

Keeps a listener of an actor’s snapshots; an empty one is none, as an empty callback is.

Synopsis

void
subscribe(
    xactor::actor_ref actor,
    snapshot_listener listener);

Parameters

Name Description

actor

The actor.

listener

The listener.

webcpp::xstate::system_state::unpark

Takes an actor out of the parked.

Synopsis

void
unpark(xactor::actor_ref actor);

Parameters

Name Description

actor

The actor.

webcpp::xstate::system_state::unregister

Releases the systemId an actor holds, and only its; XState’s system._unregister.

Synopsis

void
unregister(xactor::actor_ref actor);

Parameters

Name Description

actor

The actor.

webcpp::xstate::system_state::unregister_family

Releases the systemIds an actor and its descendants hold at once, as XState’s stopChild does before the stop itself runs.

Synopsis

void
unregister_family(xactor::actor_ref actor);

Parameters

Name Description

actor

The actor.

See Also

"Guarantees", in the guide: guarantee A5.

webcpp::xstate::transition_definition

One transition of a node.

Synopsis

struct transition_definition;

Description

It ports XState’s TransitionDefinition. A microstep reports the transitions it took as pointers to these, valid while any copy of the machine lives.

Data Members

Name Description

actions

The actions the transition runs.

config

The transition’s config, which XState spreads into its definition.

delay

A delayed transition’s delay.

event_type

The event descriptor: "" for an eventless (always) transition, the after event’s type for a delayed one.

guard

The index of the transition’s guard in machine::guard.

reenter

The config’s reenter.

source

The node the transition belongs to.

target

The nodes the transition enters, none for a targetless transition.

See Also

"Transitions", in the guide.

webcpp::xstate::transition_definition::actions

The actions the transition runs.

Synopsis

std::vector<action_ref> actions = {};

webcpp::xstate::transition_definition::config

The transition’s config, which XState spreads into its definition.

Synopsis

boost::json::object config = {};

webcpp::xstate::transition_definition::delay

A delayed transition’s delay.

Synopsis

std::optional<delay_ref> delay = {};

webcpp::xstate::transition_definition::event_type

The event descriptor: "" for an eventless (always) transition, the after event’s type for a delayed one.

Synopsis

std::string event_type = {};

webcpp::xstate::transition_definition::guard

The index of the transition’s guard in machine::guard.

Synopsis

std::optional<std::size_t> guard = {};

webcpp::xstate::transition_definition::reenter

The config’s reenter.

Synopsis

bool reenter = false;

webcpp::xstate::transition_definition::source

The node the transition belongs to.

Synopsis

std::size_t source = 0;

webcpp::xstate::transition_definition::target

The nodes the transition enters, none for a targetless transition.

Synopsis

std::optional<std::vector<std::size_t>> target = {};

Description

Note

None is a targetless transition, which XState tells apart from one whose target list is empty.

webcpp::xstate::errc

The errors xstate reports, as boost::system::error_code values of the category error_category.

Synopsis

Declared in <webcpp/xstate/errors.hpp>

enum class errc : int;

Description

The values start at 1, so an error_code of value 0, which a snapshot that has not failed holds, is no error. An errc converts to an error_code and compares with one: created.error() == xstate::errc::unknown_target.

There is no invalid_argument: the code with which the actor layer refuses a call, an address that names no actor, a stop of a child, a request no longer pending or a call made from a callback, is xactor’s webcpp::xactor::errc::invalid_argument, of the category "webcpp.xactor".

Members

Name Description

invalid_config

A config is not a machine, or an implementation or a clock was misused.

unknown_target

A target names no state, or a send names no actor.

unknown_state

A state value, a state id or a path names no state.

unknown_guard

A guard the config names has no implementation.

unknown_delay

An after delay the config names has no implementation.

implementation_failed

An implementation failed, where XState’s would have thrown.

invalid_event

An event has no string type, or a macrostep takes one of the wildcard type from outside.

actor_failed

A child actor’s error event reached a machine and no transition took it.

unknown_actor

An invoke, or a spawnChild the config names, names no actor of the implementations.

system_id_taken

An actor claims a systemId that another actor of the system holds.

See Also

"Errors as values", in the guide.

"What creation refuses", in the guide.

webcpp::xstate::errc::invalid_config

A config is not a machine, or an implementation or a clock was misused.

Synopsis

Declared in <webcpp/xstate/errors.hpp>

invalid_config = 1

Description

The config is not an object, or a member has the wrong type; an entry, exit, actions or tags, or a history state’s target, is null; a compound state has no initial; an on key is ""; a transition declares a truthy cond; an after key starts with a digit, ., - or + and is not a whole number of milliseconds; an invoke has no string src; or entering a state by default leads back to it. An implementation holds an empty function. An entry of implementations::inputs or implementations::outputs names nothing it can compute, or something the config already fixes. A definition holds a member of another type than XState writes. A simulated_clock is set back in time.

webcpp::xstate::errc::unknown_target

A target names no state, or a send names no actor.

Synopsis

Declared in <webcpp/xstate/errors.hpp>

unknown_target = 2

Description

A transition, an initial or a history state’s target names no state; a sendTo names no actor; or a microstep needs the default of a history state that is the machine’s root.

webcpp::xstate::errc::unknown_state

A state value, a state id or a path names no state.

Synopsis

Declared in <webcpp/xstate/errors.hpp>

unknown_state = 3

webcpp::xstate::errc::unknown_guard

A guard the config names has no implementation.

Synopsis

Declared in <webcpp/xstate/errors.hpp>

unknown_guard = 4

webcpp::xstate::errc::unknown_delay

An after delay the config names has no implementation.

Synopsis

Declared in <webcpp/xstate/errors.hpp>

unknown_delay = 5

webcpp::xstate::errc::implementation_failed

An implementation failed, where XState’s would have thrown.

Synopsis

Declared in <webcpp/xstate/errors.hpp>

implementation_failed = 6

Description

xstate reports it for an assign on a context that is not an object, and for a context function whose value is not an object; an implementation may return it, or any code of its own. Whichever code a failure has becomes the snapshot’s error.

webcpp::xstate::errc::invalid_event

An event has no string type, or a macrostep takes one of the wildcard type from outside.

Synopsis

Declared in <webcpp/xstate/errors.hpp>

invalid_event = 7

Description

XState’s development build refuses an event of the type * given to a macrostep, while one the machine raises takes the wildcard transitions.

webcpp::xstate::errc::actor_failed

A child actor’s error event reached a machine and no transition took it.

Synopsis

Declared in <webcpp/xstate/errors.hpp>

actor_failed = 8

Description

The snapshot’s error_value holds the child’s error, when its error event carried one.

webcpp::xstate::errc::unknown_actor

An invoke, or a spawnChild the config names, names no actor of the implementations.

Synopsis

Declared in <webcpp/xstate/errors.hpp>

unknown_actor = 9

webcpp::xstate::errc::system_id_taken

An actor claims a systemId that another actor of the system holds.

Synopsis

Declared in <webcpp/xstate/errors.hpp>

system_id_taken = 10

webcpp::xstate::history_kind

A history node’s kind, from its history member.

Synopsis

enum class history_kind : int;

Description

It is deep for "deep", shallow for any other truthy history, and none for a node that is a history node by its type alone, its history falsy or absent.

Note

A history node of the kind none records as a shallow one does, as XState records every history but 'deep' as shallow.

Members

Name Description

none

Not a history node, or one whose history is falsy or absent.

shallow

Remembers the parent’s active children.

deep

Remembers every active descendant of the parent.

See Also

"Shallow and deep history", in the guide.

webcpp::xstate::history_kind::none

Not a history node, or one whose history is falsy or absent.

Synopsis

none

webcpp::xstate::history_kind::shallow

Remembers the parent’s active children.

Synopsis

shallow

webcpp::xstate::history_kind::deep

Remembers every active descendant of the parent.

Synopsis

deep

webcpp::xstate::node_type

A node’s type, as XState’s StateNode decides it.

Synopsis

enum class node_type : int;

Description

It is the config’s type, or, without one, compound for a node with children, history for a node whose history member is truthy as JavaScript reads it, and atomic otherwise: a history of false, null, 0 or "" leaves a node atomic.

Members

Name Description

atomic

A state without children.

compound

A state with children, one of which is active while it is.

parallel

A state with children, all of which are active while it is.

final

A state that, once entered, makes its parent done.

history

A pseudo-state that enters the children its parent was last in.

See Also

"States", in the guide.

webcpp::xstate::node_type::atomic

A state without children.

Synopsis

atomic

webcpp::xstate::node_type::compound

A state with children, one of which is active while it is.

Synopsis

compound

webcpp::xstate::node_type::parallel

A state with children, all of which are active while it is.

Synopsis

parallel

webcpp::xstate::node_type::final

A state that, once entered, makes its parent done.

Synopsis

final

webcpp::xstate::node_type::history

A pseudo-state that enters the children its parent was last in.

Synopsis

history

webcpp::xstate::run_outcome

What a host call left: every actor settled, or one parked for want of fuel.

Synopsis

enum class run_outcome : int;

Members

Name Description

settled

Every actor has done what it was given, and no message is left.

out_of_fuel

An actor parked because its execution could not pay for its next piece of work, which actor_system::resume continues.

See Also

"Fuel, parking and resume", in the guide.

"Guarantees", in the guide: guarantee A1.

webcpp::xstate::run_outcome::settled

Every actor has done what it was given, and no message is left.

Synopsis

settled

webcpp::xstate::run_outcome::out_of_fuel

An actor parked because its execution could not pay for its next piece of work, which actor_system::resume continues.

Synopsis

out_of_fuel

webcpp::xstate::status

A snapshot’s status.

Synopsis

enum class status : int;

Description

It ports XState’s snapshot.status. XState’s fourth status, stopped, has no counterpart here: XState’s macrostep gives it, through transition too, to a snapshot that receives the event xstate.stop, while xstate runs xstate.stop as any other event. For an actor of the actor layer, actor_system::status_of tells it, as a webcpp::xactor::status.

Members

Name Description

active

The machine runs.

done

The machine reached a final state at the top level, with its output, when it has one.

error

A macrostep failed, the snapshot’s error saying why.

See Also

"The snapshot", in the guide.

webcpp::xstate::status::active

The machine runs.

Synopsis

active

webcpp::xstate::status::done

The machine reached a final state at the top level, with its output, when it has one.

Synopsis

done

webcpp::xstate::status::error

A macrostep failed, the snapshot’s error saying why.

Synopsis

error

webcpp::xstate::begin

Makes the cursor of the macrostep an event runs from a snapshot.

Synopsis

macrostep
begin(
    machine const& owner,
    snapshot from,
    event happened);

Description

Its first macrostep::next runs the event’s microstep. An event that no transition takes leaves the snapshot unchanged and reports an empty list of transitions. The macrostep settles there unless the event enables an eventless transition, which a guard that reads the event can do: XState’s macrostep selects the eventless transitions after the event’s microstep, whether or not that microstep took a transition.

Return Value

The cursor, which has run no microstep yet.

Parameters

Name Description

owner

The machine.

from

The snapshot the macrostep begins from.

happened

The event.

Preconditions

  • from is a snapshot of owner.

See Also

"One microstep at a time", in the guide.

webcpp::xstate::begin_initial

Makes the cursor of a machine’s initial macrostep.

Synopsis

macrostep
begin_initial(
    machine const& owner,
    boost::json::value const& input = nullptr);

Description

It ports XState’s getInitialSnapshot and _getPreInitialState, one microstep a call: the initial microstep, which enters the initial states, then XState’s loop for the event xstate.init, which carries input as its member input when it is not null. The initial context is owner.context(), or, when implementations::context is set, what it returns for input. The cursor’s macrostep::began_from is the snapshot before the initial microstep, whose nodes hold the root alone.

Note

A context that cannot be computed, because the context function fails or returns a value that is not an object (errc::implementation_failed), fails the macrostep on its first macrostep::next, with the snapshot before it, as XState’s getInitialSnapshot catches the error into the snapshot it returns.

Return Value

The cursor, which has run no microstep yet.

Parameters

Name Description

owner

The machine.

input

The machine’s input, null for none.

See Also

"The initial macrostep", in the guide.

"A machine’s input", in the guide.

webcpp::xstate::can

Whether an event would take, from a snapshot, a transition that does something.

Synopsis

result<bool>
can(
    machine const& owner,
    snapshot const& from,
    event const& happened);

Description

It ports XState’s snapshot.can(event): a transition does something when it has a target or actions. It evaluates the guards, as XState does, and runs no action.

Return Value

Whether a transition that does something would be taken; a guard’s failure, or errc::unknown_state when the value of from, or a stateIn guard’s #id, names no state of the machine.

Parameters

Name Description

owner

The machine.

from

The snapshot.

happened

The event.

Preconditions

  • from is a snapshot of owner.

See Also

"Whether an event would do anything", in the guide.

webcpp::xstate::create_machine

Builds a machine from the JSON of XState’s createMachine config, with the implementations its names stand for.

Synopsis

result<machine>
create_machine(
    boost::json::value const& config,
    implementations registry);

Description

It ports XState’s setup({ ... }).createMachine(config). A config is refused here, at once, with an error and no machine, including what XState refuses only later, when it first evaluates a guard, enters a state or runs a list of actions. Two things are checked only when they are used, as in XState: the #id of an xstate.stateIn guard, which fails the macrostep or can with errc::unknown_state, and the default of a history state that is the machine’s root, which has no parent to resolve it against and fails the microstep that needs it with errc::unknown_target. A machine whose root is a history state is accepted, as XState accepts it. A history state below the root, a child of the root included, resolves its default against its parent.

A config’s states, on and after are read in the order JavaScript enumerates an object’s keys, as XState reads them: a key that is an array index, such as "0" or "500", first, by number, then the rest as written. That order is the document order of the states, and so the order in which a parallel state’s regions are entered; the order of a state’s transitions; and the order of the raises its after returns on entry, so an after that writes "1000" before "500" returns the raise of 500 first.

It fails with one of these errors:

  • errc::invalid_config when the config is not an object or a member has the wrong type; an entry or an exit, or the actions of a transition or of an object initial, is null, which XState’s toArray reads as a list that holds null and fails on when it runs it; a tags, or a history state’s target, is neither a string nor a list of strings, null included, the target of a history at the root too; a compound state has no initial; an on key is ""; a transition declares a truthy cond, renamed guard in XState v5; an after key starts with a digit, ., - or + and is not a whole number of milliseconds ("1.5", "-5", and also "5abc", which XState would read as the name of a delay); an invoke has no string src; entering a state by default leads back to it; an implementation holds an empty function; an entry of implementations::inputs or implementations::outputs names what it may not;

  • errc::unknown_target when a transition, an initial or a history state’s target names no state;

  • errc::unknown_guard when a guard the config names is not in implementations::guards;

  • errc::unknown_delay when an after key names no entry of implementations::delays;

  • errc::unknown_actor when an invoke’s src, or the src of a spawn_child_action the config names, is not in implementations::actors.

Note

Nodes are built in document order, a parent before its children, and the children in the order JavaScript enumerates the keys of states, which is XState’s order; transitions are formatted once every node exists, because a target may name any id.

Return Value

The machine, or the error that refused the config.

Parameters

Name Description

config

The config, a JSON object.

registry

The implementations the config’s names stand for.

See Also

"Creating a machine", in the guide.

"The config", in the guide.

"What creation refuses", in the guide.

"A config is refused when it is created", in the guide.

webcpp::xstate::create_machine_from_definition

Loads a machine back from a definition, with the implementations and the context, which a definition does not hold.

Synopsis

result<machine>
create_machine_from_definition(
    boost::json::value const& definition,
    implementations registry,
    boost::json::value context);

Description

The definition is one to_json, or XState’s machine.toJSON(), wrote. A definition also leaves out the eventless transitions and a history state’s default target, and one that XState wrote leaves out the higher-order guards as well; two states may share an id when a key holds a dot. The loader drops every entry or exit action of the type xstate.raise or xstate.cancel, since the raise and the cancel an after adds are written so and the after it rebuilds adds them again; an entry or exit action the config named so is lost with them. A machine loaded from the definition of one that has none of those takes the original’s steps, and its to_json writes what the original’s writes, the version and the meta and description of each initial included. A node’s key, order and transitions in the definition are not read.

Return Value

The machine; errc::invalid_config when definition is not an object, or a node’s states is not an object of objects, its on not an object of lists of objects, its entry or exit not a list, its invoke not a list of objects, or the first target of its initial not a string, a member of another type than XState writes; or any error of create_machine for the config it rebuilds, which checks what the other members put there, such as a node’s id, type and tags.

Parameters

Name Description

definition

The definition, as to_json writes it.

registry

The implementations the definition’s names stand for.

context

The machine’s initial context.

See Also

"The definition", in the guide.

webcpp::xstate::ending_event

The event an actor that ends sends its parent.

Synopsis

event
ending_event(
    std::string_view kind,
    std::string_view id,
    std::string_view member,
    std::optional<boost::json::value> const& value);

Description

Its type is xstate.<kind>.actor.<id>, and its payload holds value under member when there is one, then actorId, the id. The layer calls it with "done" and "output", or "error" and "error": XState’s createDoneActorEvent and createErrorActorEvent.

Return Value

The event.

Parameters

Name Description

kind

"done" or "error".

id

The ending actor’s id, as its parent knows it.

member

The payload member that holds value.

value

The output or the error; none for JavaScript’s undefined, which leaves member out.

See Also

"Done and error events", in the guide.

"Guarantees", in the guide: guarantee A6.

webcpp::xstate::error_category

The category of every errc.

Synopsis

Declared in <webcpp/xstate/errors.hpp>

boost::system::error_category const&
error_category() noexcept;

Description

Its name() is "webcpp.xstate", and its message(value) the name of the enumerator, "unknown_target" for errc::unknown_target, or "unknown" for a value errc does not define.

Return Value

The category, a constant with a fixed identity.

webcpp::xstate::event_from_json

Reads an event from XState’s flat JSON form.

Synopsis

Declared in <webcpp/xstate/event.hpp>

result<event>
event_from_json(boost::json::value const& json);

Description

json is an object whose member type is a string, and every other member goes to the payload, in order.

Note

An event of the wildcard type is read like any other; only a macrostep that takes one from outside refuses it, as XState’s does.

Return Value

The event; errc::invalid_event when json is not an object, or has no type, or its type is not a string.

Parameters

Name Description

json

The event as XState writes it.

webcpp::xstate::failure

Builds a failed result of the type the call would have returned.

Synopsis

Declared in <webcpp/xstate/errors.hpp>

template<class T>
result<T>
failure(errc code) noexcept;

Description

It is the way an implementation reports a failure: return xstate::failure<bool>(xstate::errc::implementation_failed);. An implementation may return any other error_code as well; it becomes the snapshot’s error as it is.

Return Value

A result that holds make_error_code(code).

Template Parameters

Name Description

T

The type of the value the result would hold.

Parameters

Name Description

code

The error.

webcpp::xstate::get_initial_microsteps

Runs a machine’s initial macrostep and returns every microstep.

Synopsis

std::vector<microstep>
get_initial_microsteps(
    machine const& owner,
    boost::json::value const& input = nullptr);

Description

It ports XState’s getInitialMicrosteps: begin_initial run to its end, without a fuel bound.

Return Value

Every microstep of the initial macrostep, in order.

Parameters

Name Description

owner

The machine.

input

The machine’s input, null for none.

See Also

"The microsteps of the initial state", in the guide.

webcpp::xstate::get_initial_snapshot

Runs a machine’s initial macrostep and returns the snapshot it starts in.

Synopsis

snapshot
get_initial_snapshot(
    machine const& owner,
    boost::json::value const& input = nullptr);

Description

It ports XState’s getInitialSnapshot: begin_initial run to its end, without a fuel bound. A machine whose initial macrostep fails comes back in a snapshot whose status is status::error.

Return Value

The settled snapshot.

Parameters

Name Description

owner

The machine.

input

The machine’s input, null for none.

See Also

"Computing the next state", in the guide.

webcpp::xstate::get_meta

The meta of every active node that has one, by node id.

Synopsis

boost::json::object
get_meta(
    machine const& owner,
    snapshot const& of);

Description

It ports XState’s snapshot.getMeta(). The members are in the order of of.nodes.

Return Value

The object of the active nodes’ meta, keyed by node id.

Parameters

Name Description

owner

The machine.

of

A snapshot of owner.

Preconditions

  • of is a snapshot of owner.

See Also

"Meta and descriptions", in the guide.

webcpp::xstate::get_microsteps

Runs the macrostep of an event and returns every microstep.

Synopsis

std::vector<microstep>
get_microsteps(
    machine const& owner,
    snapshot const& from,
    event const& happened);

Description

It ports XState’s getMicrosteps: begin run to its end, without a fuel bound.

Return Value

Every microstep of the macrostep, in order.

Parameters

Name Description

owner

The machine.

from

The snapshot the macrostep begins from.

happened

The event.

Preconditions

  • from is a snapshot of owner.

See Also

"The microsteps of an event", in the guide.

webcpp::xstate::get_next_snapshot

Runs the macrostep of an event and returns the snapshot it leads to.

Synopsis

snapshot
get_next_snapshot(
    machine const& owner,
    snapshot const& from,
    event const& happened);

Description

It ports XState’s getNextSnapshot: begin run to its end, without a fuel bound.

Return Value

The settled snapshot.

Parameters

Name Description

owner

The machine.

from

The snapshot the macrostep begins from.

happened

The event.

Preconditions

  • from is a snapshot of owner.

See Also

"The snapshot alone", in the guide.

webcpp::xstate::get_next_transitions

Lists every transition a snapshot could take, guards not evaluated.

Synopsis

std::vector<transition_definition const*>
get_next_transitions(
    machine const& owner,
    snapshot const& from);

Description

It ports XState’s getNextTransitions. For each active atomic node, it lists its own transitions and its ancestors’, each node once: a node’s event transitions first, in the order of state_node::transitions, and its eventless ones after.

Return Value

The transitions, as pointers into the machine.

Parameters

Name Description

owner

The machine.

from

The snapshot.

Preconditions

  • from is a snapshot of owner.

See Also

"Every transition a snapshot could take", in the guide.

webcpp::xstate::initial_transition

Runs a machine’s initial macrostep and returns its snapshot and its actions.

Synopsis

std::pair<snapshot, std::vector<action>>
initial_transition(
    machine const& owner,
    boost::json::value const& input = nullptr);

Description

It ports XState’s initialTransition: begin_initial run to its end, without a fuel bound. The actions are macrostep_result::actions. A caller checks the snapshot’s status before it executes them: of an initial macrostep that failed, XState’s actor has executed none.

Return Value

The settled snapshot, and every action of the macrostep.

Parameters

Name Description

owner

The machine.

input

The machine’s input, null for none.

See Also

"Computing the next state", in the guide.

"Actions", in the guide.

webcpp::xstate::make_error_code

Makes the error_code of an errc, as its implicit conversion does.

Synopsis

Declared in <webcpp/xstate/errors.hpp>

boost::system::error_code
make_error_code(errc code) noexcept;

Return Value

code as an error_code of the category error_category.

Parameters

Name Description

code

The error.

webcpp::xstate::matches_state

Whether the state value child is parent or a state within it.

Synopsis

bool
matches_state(
    boost::json::value const& parent,
    boost::json::value const& child);

Description

It ports XState’s matchesState, its recursion a list of the pairs still to compare. Either value may be a state value or a dotted string. The parent "b" matches the child {"b": "b1", "c": "c1"}, and the parent "a.b" matches the child {"a": {"b": "c"}}; the parent {"a": {"b": "c"}} does not match the child "a.b". snapshot::matches is this function with the snapshot’s value as child.

Return Value

true when child is parent or a state within it.

Parameters

Name Description

parent

The state value that may hold child.

child

The state value that may lie within parent.

webcpp::xstate::path_to_state_value

Builds the state value a path describes.

Synopsis

boost::json::value
path_to_state_value(std::span<std::string const> path);

Description

It ports XState’s pathToStateValue: ["a", "b", "c"] is {"a": {"b": "c"}}, ["a"] is "a", and an empty path is {}.

Return Value

The state value.

Parameters

Name Description

path

The keys from the root down.

webcpp::xstate::resolve_state

Builds the snapshot of a state value, completed to every node it implies, with a context.

Synopsis

result<snapshot>
resolve_state(
    machine const& owner,
    boost::json::value const& state_value,
    boost::json::value context);

Description

It ports XState’s machine.resolveState({ value, context }). The value is completed with a compound state’s initial child, a parallel state’s regions and every ancestor. The snapshot’s status is status::done when the value is in a final state at the top level and status::active otherwise; it has no output, no history and no children. Its snapshot::nodes are in the order XState’s resolveState gives them, which for a parallel state differs from the order a microstep gives: each level’s named children before their descendants, so the done value {"a": "a2", "b": "b2"} of a parallel root p gives p, p.a, p.b, p.a.a2, p.b.b2.

The keys of an object value are read in the order JavaScript enumerates them, as XState’s getStateNodes reads Object.keys: of a parallel root p with the regions "3" and "z", the value {"z": "z2", "3": "d"} gives p.3 before p.z, and the snapshot’s value is {"3": "d", "z": "z2"}, in both libraries. A string value names a child of the root by its key, as in XState, so "a.b" is the key a.b, not a path; a nested value is written as an object.

Return Value

The snapshot; errc::unknown_state when the value names a state the machine does not have, or is neither a string nor an object.

Parameters

Name Description

owner

The machine.

state_value

The state value, a string or an object.

context

The snapshot’s context.

See Also

"Persisted state", in the guide.

webcpp::xstate::to_json

to_json overloads

Synopses

Writes an event in XState’s flat JSON form.

boost::json::object
to_json(event const& written);

Writes a machine’s definition, key for key as XState’s machine.toJSON() writes it.

boost::json::value
to_json(machine const& owner);

Return Value

  • The event as XState writes it.

  • The definition of the root, which holds every node’s.

Parameters

Name Description

written

The event.

owner

The machine.

See Also

"The definition", in the guide.

webcpp::xstate::to_json

Writes an event in XState’s flat JSON form.

Synopsis

Declared in <webcpp/xstate/event.hpp>

boost::json::object
to_json(event const& written);

Description

type comes first, then the payload’s members in order. A payload member named type is not written, since the event’s type holds that key.

Return Value

The event as XState writes it.

Parameters

Name Description

written

The event.

webcpp::xstate::to_json

Writes a machine’s definition, key for key as XState’s machine.toJSON() writes it.

Synopsis

boost::json::value
to_json(machine const& owner);

Description

For each node it writes its id, key, type, initial, states, history, on, transitions, entry, exit, order (the root’s written -1, as XState writes order || -1), invoke and tags, with its meta, output and description and the config’s version when there are any. A node’s history is written as XState writes it: true as "shallow", a falsy or absent one as false, and any other value as the config gave it. A node’s initial holds the meta and description an object initial gives. The higher-order guards are written where a transition’s config holds them, where XState writes nothing, its guards being functions. What XState’s definition leaves out, this one leaves out too: the context, the eventless transitions and a history state’s default target.

Note

The definition is built deepest first, so each node’s children are written before it.

Return Value

The definition of the root, which holds every node’s.

Parameters

Name Description

owner

The machine.

See Also

"The definition", in the guide.

webcpp::xstate::to_state_path

Splits a state id into the keys of its path.

Synopsis

std::vector<std::string>
to_state_path(std::string_view id);

Description

It ports XState’s toStatePath. The id is split at each dot, a backslash making the character after it part of the segment, so a key may hold a dot; an empty id is the path of one empty key:

to_state_path("a\\.b.c");  // {"a.b", "c"}
to_state_path("");         // {""}

Return Value

The segments, at least one.

Parameters

Name Description

id

The state id, or a dotted path.

webcpp::xstate::to_state_value

Converts a state value given as a dotted string to its object form.

Synopsis

boost::json::value
to_state_value(boost::json::value const& given);

Description

It ports XState’s toStateValue: "a.b.c" is {"a": {"b": "c"}}, and any other value is returned as it is.

Return Value

The state value.

Parameters

Name Description

given

A state value, or a dotted string.

webcpp::xstate::transition

Runs the macrostep of an event and returns the snapshot it leads to and its actions.

Synopsis

std::pair<snapshot, std::vector<action>>
transition(
    machine const& owner,
    snapshot const& from,
    event const& happened);

Description

It ports XState’s transition: begin run to its end, without a fuel bound. A caller checks the snapshot’s status before it executes the actions: of a macrostep that failed, XState’s actor has executed only the custom actions and logs.

Return Value

The settled snapshot, and every action of the macrostep.

Parameters

Name Description

owner

The machine.

from

The snapshot the macrostep begins from.

happened

The event.

Preconditions

  • from is a snapshot of owner.

See Also

"Computing the next state", in the guide.

"Actions", in the guide.

webcpp::xstate::init_event_type

The type of the event every initial macrostep runs with, "xstate.init".

Synopsis

Declared in <webcpp/xstate/event.hpp>

inline constexpr std::string_view init_event_type = "xstate.init";

Description

It ports XState’s XSTATE_INIT. The event carries the machine’s input as its member input when the input is not null.

Note

For an event of this type that a caller passes to begin, the two libraries differ: XState’s macrostep takes no transition for it, only the eventless transitions it then finds enabled, while xstate’s begin takes its transitions as any event’s.

See Also

"The xstate.init event", in the guide.

webcpp::xstate::wildcard

The descriptor that matches every event, "*".

Synopsis

Declared in <webcpp/xstate/event.hpp>

inline constexpr std::string_view wildcard = "*";

Description

It ports XState’s WILDCARD. A state’s transitions under the on key "*" are candidates for any event, after those under the event’s own type and those under a longer partial descriptor such as "mouse.*". An event may have the wildcard as its type: a macrostep refuses one it takes from outside with errc::invalid_event, as XState’s development build does, while one the machine raises takes the wildcard transitions.

See Also

"Event descriptors", in the guide.

webcpp::xstate::actor_ref

An actor’s address in its system: webcpp::xactor::actor_ref, brought into this namespace.

Synopsis

using xactor::actor_ref;

Description

Its one member, value, is a number, not a handle: it keeps nothing alive, and actor_system::status_of tells whether its actor still runs. Addresses start at 1, and 0 names no actor. It stands for XState’s ActorRef.

See Also

"Actor references become actor_ref", in the guide.

webcpp::xstate::budgets

The fuel of one execution of the system: webcpp::xactor::budgets, brought into this namespace.

Synopsis

using xactor::budgets;

Description

Its one member, fuel, is what each execution starts with, and nothing refills it. Every microstep an actor runs costs one unit, and so does every message an actor sends (an event, a child’s construction and its answer, a child’s start, a stop) and every timer it arms. Each host call that delivers opens executions of its own, each with the whole of it.

See Also

"Fuel, parking and resume", in the guide.

"Guarantees", in the guide: guarantee A3.

WEBCPP_XSTATE_NO_EXCEPTIONS

Defined when xstate is built without exceptions: when webcpp has none (the compiler has none, or the developer defined WEBCPP_NO_EXCEPTIONS for every webcpp library), or when the developer defines it alone, to disable xstate’s in a build that has them.

Synopsis

Declared in <webcpp/xstate/config.hpp>

#define WEBCPP_XSTATE_NO_EXCEPTIONS

Description

xstate raises no exception of its own: where XState throws, it returns an error, so the macro changes nothing in it. It is provided so that a program can set it for every webcpp library alike. An exception xstate raised would go through boost::throw_exception, and an API that throws would be absent while this is defined.

See Also

"How xstate is tested", in the guide.

Appendix A: From XState to C++

This appendix is for a reader who knows XState. It shows how each of XState’s JavaScript spellings is written in C++: a machine’s config, the functions the config holds, an event, an actor, a promise and an error. The machine itself does not change: its states, its transitions and what they mean are XState’s. What changes is how a program writes them, because C++ has no object literal, a JSON config holds no function, and xstate throws nothing.

Each section gives XState’s spelling first, in inline code, a table or a program, and then xstate’s, from an example program of this page. A program of XState’s shown here is the twin of the C++ program after it: the same program, which ran on XState 5.33.2 and printed what the C++ program prints (A twin for every example). The last section, XState’s names, goes from each of XState’s names to xstate’s.

Names keep XState’s words, in snake case: createMachine is create_machine, getNextSnapshot is get_next_snapshot, and snapshot.hasTag is snapshot.has_tag. The C++ examples write namespace xstate = webcpp::xstate;, and every value a machine holds, its config, its context, an event’s payload, an action’s params, an input and an output, is a boost::json::value.

A JavaScript object becomes a JSON config

XState’s createMachine takes the config as a JavaScript object, written in the program. xstate’s create_machine takes the same config as JSON, a boost::json::value the program parses from text, reads from a file or receives in a request. C++ has no object literal to write a config in, and as data a config can come from anywhere, Stately Studio’s export included, and needs no compiler (A JSON config).

The toggle of the quick start is an object in XState:

const toggleMachine = createMachine({
  id: 'toggle',
  initial: 'Inactive',
  states: {
    Inactive: {
      on: { toggle: 'Active' },
    },
    Active: {
      on: { toggle: 'Inactive' },
    },
  },
});

and JSON in xstate:

const boost::json::value config = boost::json::parse(R"({
    "id": "toggle",
    "initial": "Inactive",
    "states": {
        "Inactive": {"on": {"toggle": "Active"}},
        "Active": {"on": {"toggle": "Inactive"}}
    }
})");
const xstate::result<xstate::machine> toggle_machine = xstate::create_machine(config, {});
if (!toggle_machine.has_value()) {
    std::cout << "refused: " << toggle_machine.error().message() << '\n';
    return 1;
}

Every member keeps its name and its meaning; JSON changes how it is written, and a function becomes a name:

XState’s object xstate’s JSON

id: 'toggle', a bare key and a string in single quotes

"id": "toggle", every key and every string in double quotes

after: { 2000: 'Inactive' }

"after": {"2000": "Inactive"}: a key is always a string

entry: assign(...), guard: ({ context }) => ...

"entry": "incrementCount", "guard": "countBelowMax": a name, and the function registered under it (Functions become implementations registered by name)

{ type: 'notify', params: { level: 2 } }

{"type": "notify", "params": {"level": 2}}, its params fixed in the config

not('isEmpty'), stateIn('#light.green')

{"type": "xstate.not", "guards": ["isEmpty"]}, {"type": "xstate.stateIn", "stateValue": "#light.green"} (Higher-order guards and stateIn)

a member that is undefined

the member left out (undefined in JSON)

The rules:

  • create_machine returns a result<machine>: the machine, or the reason the config was refused, which the program tests where a program of XState’s would catch what createMachine throws (Exceptions become result). It refuses every config XState refuses, and some XState accepts and fails on only later (A config is refused when it is created).

  • The keys of states, on and after are read in the order JavaScript enumerates an object’s keys, as XState reads them: a key that is an array index, such as "500", first, by number, then the rest as written (The config).

  • A machine never changes once created. XState’s machine.provide() is the same config created again with other implementations (A machine is immutable), and machine.toJSON() is to_json(machine), which create_machine_from_definition loads back (The definition).

  • Appendix B lists every place where a JSON config takes another form than XState’s object, each with its reason (What JSON imposes).

Functions become implementations registered by name

XState’s config may hold functions: an action, a guard, a delay, the logic of an invoked actor, and the functions that compute the initial context, an invoked child’s input and a final state’s output. JSON holds none, so xstate’s config names each one, and the program registers its C++ code under that name in an implementations, which create_machine takes with the config. XState already splits a machine this way when setup() registers implementations and the config refers to them by name: there, setup({ ... }).createMachine(config) becomes create_machine(config, implementations), and the config stays as it is (Implementations registered by name).

Stately’s setup example registers two assign actions, which the config names under on:

const counter = setup({
  actions: {
    increment: assign({
      count: ({ context }) => context.count + 1,
    }),
    decrement: assign({
      count: ({ context }) => context.count - 1,
    }),
  },
}).createMachine({
  context: { count: 0 },
  on: {
    inc: { actions: 'increment' },
    dec: { actions: 'decrement' },
  },
});

let [now] = initialTransition(counter);
for (const type of ['inc', 'inc', 'dec']) {
  [now] = transition(counter, now, { type });
  console.log(type, JSON.stringify(now.context));
}

xstate registers each as an assign_action, whose function returns the members to merge into the context; add, a function of the program, computes them:

/** The context's count plus `step`, as an assign merges it into the context. */
xstate::result<boost::json::object> add(const xstate::action_args& args, std::int64_t step) {
    const boost::json::object* context = args.context.if_object();
    const boost::json::value* count = context == nullptr ? nullptr : context->if_contains("count");
    const std::int64_t* value = count == nullptr ? nullptr : count->if_int64();
    if (value == nullptr) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"count", *value + step}};
}

const xstate::assign_action increment{
    .assignment = [](const xstate::action_args& args) { return add(args, 1); },
};
const xstate::assign_action decrement{
    .assignment = [](const xstate::action_args& args) { return add(args, -1); },
};
xstate::implementations implementations;
implementations.actions.emplace("increment", increment);
implementations.actions.emplace("decrement", decrement);

const boost::json::value config = boost::json::parse(R"({
    "context": {"count": 0},
    "on": {
        "inc": {"actions": "increment"},
        "dec": {"actions": "decrement"}
    }
})");
const xstate::result<xstate::machine> counter = xstate::create_machine(config, implementations);
if (!counter.has_value()) {
    return 1;
}

xstate::snapshot now = xstate::initial_transition(*counter).first;
for (const std::string_view type : {"inc", "inc", "dec"}) {
    const xstate::event sent{.type = std::string(type), .payload = {}};
    now = xstate::transition(*counter, now, sent).first;
    std::cout << type << ' ' << boost::json::serialize(now.context) << '\n';
}

It prints:

inc {"count":1}
inc {"count":2}
dec {"count":1}

implementations has a member for each kind of function. actions, guards, delays and actors are keyed by name, as setup() keys them. The other three stand for functions XState’s config holds in place, so each is keyed by the place: context, one function, computes the initial context from the input; inputs computes a child’s input, under the id of its invoke; and outputs computes a final state’s output, under the id of that state, or the machine’s under the root’s id. Each member is taught in the section on implementations. Each function keeps its role in XState and takes a C++ shape:

XState xstate

An action creator, such as assign({ count: ({ context }) => context.count + 1 })

The built-in action of the same name, such as an assign_action, holding a function that returns what XState’s creator computes: here the members to merge, boost::json::object{{"count", ...}} (Built-in and custom actions)

An action that is a plain function, such as () => console.log('done')

No entry: an action name actions does not hold is a custom action, returned to the program, which performs it (Custom actions)

A guard, ({ context, event }, params) => boolean

A predicate: a function of an action_args, which returns result<bool>

A delay, ({ context, event }) => number

A delay_maker, which returns result<std::uint64_t>, in milliseconds

src: childMachine, src: fromPromise(...)

A machine_actor{child_machine}, or a host_actor{} (Promises become host actors)

context: ({ input }) => ({ ... })

implementations.context: a context_maker, a function of the input that returns result<boost::json::value>

An invoke’s input: ({ context }) => ..., a final state’s output: ({ context }) => ...

A value_maker, in inputs or outputs, which returns result<std::optional<boost::json::value>>, none for undefined

The rules:

  • Every implementation receives an action_args: args.context, args.event and args.params, XState’s ({ context, event }, params). It receives no actor and no system, where XState’s also receives self and system.

  • Every implementation returns a result, and fails by returning an error where XState’s would throw (Exceptions become result).

  • A name is checked when the machine is created. A guard, an after delay or an actor that the config names and implementations does not hold refuses the machine, where XState fails when it first needs the name, or never (A config is refused when it is created). An action name is not checked: one that actions does not hold is a custom action, by design.

  • params are fixed in the config. Where XState computes them with a function of the context and the event, xstate’s implementation reads the two from its action_args (Functions become names).

A flat event object becomes event{type, payload}

An event in XState is one object: its type, the string that names what happened, and its data, the other members beside the type. xstate’s event holds the two apart: type, a std::string, and payload, a boost::json::object holding every other member. Transitions match the type alone, and the payload is JSON, as every value a machine holds is (Events).

Stately’s feedback machine receives an event that carries the new feedback. XState writes the event as one object:

const [next, actions] = transition(feedbackMachine, initial, {
  type: 'feedback.update',
  feedback: 'Some other feedback',
});

xstate writes its type and its payload:

const xstate::event update{
    .type = "feedback.update",
    .payload = {{"feedback", "Some other feedback"}},
};
const auto [next, actions] = xstate::transition(*feedback_machine, initial, update);

An implementation reads the data from the payload. XState’s assigner reads event.feedback:

updateFeedback: assign({
  feedback: ({ event }) => event.feedback,
}),

xstate’s reads the member feedback of args.event.payload, and fails when the event does not carry it:

xstate::result<boost::json::object> update_feedback(const xstate::action_args& args) {
    const boost::json::value* feedback = args.event.payload.if_contains("feedback");
    if (feedback == nullptr) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"feedback", *feedback}};
}

The program prints the context before and after the event, and the number of actions the step returned:

{"feedback":"Some feedback","rating":5}
{"feedback":"Some other feedback","rating":5}
0 actions

The rules:

  • An event with no data, XState’s { type: 'toggle' }, is xstate::event{.type = "toggle", .payload = {}}.

  • An event that arrives as JSON in XState’s flat form, from a request or a file, is read by event_from_json and written back by to_json (Events).

  • The events xstate makes carry their data in the payload too: the output of a done event and the error of an error event, each beside actorId, and the input of xstate.init. XState’s event.output is the member output of args.event.payload (Done events).

  • An event carries no actor, since a payload is JSON (Actor references become actor_ref).

Actor references become actor_ref

XState’s createActor returns an actor, an object whose methods start it, send it events, read and subscribe to its snapshot, and stop it. In xstate, actors live in an actor_system the program creates, and create_actor returns an actor_ref, a number that names one actor of that system. Each method of XState’s actor becomes a member of the system that takes the reference first: actor.send(event) is system.send(actor, event). XState creates a system too, implicitly, for each root actor; xstate makes it explicit, because the system owns what the host decides: the fuel each call may spend, the time, and the systemIds its actors share (Creating an actor).

The quick start’s toggle, run as an actor. XState creates it and calls its methods, where toggle is the event { type: 'toggle' }:

const actor = createActor(toggleMachine);

actor.subscribe((snapshot) => {
  console.log('Value:',
    JSON.stringify(snapshot.value));
});

actor.start(); // logs "Inactive"
actor.send(toggle); // logs "Active"
actor.send(toggle); // logs "Inactive"

const now = actor.getSnapshot().value;
console.log('Now:', JSON.stringify(now));

xstate creates it in a system, and names it by the actor_ref that create_actor returned:

xstate::actor_system system({.fuel = 1000});

/** Whether a call that delivers ran to its end: no error, and no actor left parked. */
const auto settled = [](const xstate::result<xstate::run_outcome>& ran) {
    return ran.has_value() && *ran == xstate::run_outcome::settled;
};

const xstate::result<xstate::actor_ref> actor = system.create_actor(*toggle_machine);
if (!actor.has_value()) {
    return 1;
}

const xstate::result<void> subscribed =
    system.subscribe(*actor, [](const xstate::snapshot& snapshot) {
        std::cout << "Value: " << boost::json::serialize(snapshot.value) << '\n';
    });
if (!subscribed.has_value()) {
    return 1;
}

if (!settled(system.start(*actor))) {  // logs "Inactive"
    return 1;
}

const xstate::event toggle{.type = "toggle", .payload = {}};
if (!settled(system.send(*actor, toggle))) {  // logs "Active"
    return 1;
}
if (!settled(system.send(*actor, toggle))) {  // logs "Inactive"
    return 1;
}

const xstate::snapshot* now = system.snapshot_of(*actor);
if (now == nullptr) {
    return 1;
}
std::cout << "Now: " << boost::json::serialize(now->value) << '\n';

It prints:

Value: "Inactive"
Value: "Active"
Value: "Inactive"
Now: "Inactive"

The table of XState’s names gives each method of XState’s actor and the member of actor_system that does its work. The rules:

  • An actor_ref is a value, copied and compared as a number is, and keeps nothing alive. An address is never reused, so a reference to an actor that has ended still names it: status_of says how it ended, and an event sent to it is dropped, as XState drops an event sent to a stopped actor. A reference that names no actor of the system is refused with invalid_argument (webcpp::xstate::actor_ref).

  • XState’s actor.getSnapshot() returns a snapshot. snapshot_of(actor) returns a pointer to one, nullptr until the actor has started (The root is constructed on start).

  • XState’s actor.send returns nothing. system.send, like every call that delivers something, runs the system and returns a result<run_outcome>: settled, out_of_fuel or an error (Create an actor and send events).

  • A config names an actor by a string and never holds one: a child by its id, the parent as #_parent, and any actor by its systemId as #system:<systemId> (Targets that name actors).

  • XState’s snapshot.children holds its children’s references. xstate’s holds their ids and srcs, and child_of(actor, id) returns a child’s reference; XState’s actor.system.get(systemId) is system.get(systemId).

Promises become host actors

XState runs asynchronous work, a request to a server or a file to read, as a promise actor: fromPromise makes actor logic of a function that returns a promise, setup({ actors }) registers it, and an invoke or a spawn starts it. When the promise resolves, the parent receives xstate.done.actor.<id> with its output; when it rejects, xstate.error.actor.<id> with its error. A C++ library has no promise to await, and with no event loop and no thread of its own it cannot wait for one. So the work is the host’s. A host_actor, registered under the actor’s name, stands for the promise: once started, it is a pending request that the host lists with host_requests(), works on at its own pace, and answers with resolve(request, output) or reject(request, error), and the parent receives the same events (Host actors).

Stately’s user machine invokes a promise that fetches a user. XState registers the promise with fromPromise. This one does not fetch: it hands its resolve and reject to requests, an array of the program’s, so that the program settles it from outside, as a host answers:

const userMachine = setup({
  actors: {
    fetchUser: fromPromise(
      ({ input, self }) =>
        new Promise((resolve, reject) => {
          requests.push({ src: self.src, id: self.id, input, resolve, reject });
        }),
    ),
  },
}).createMachine({
  id: 'user',
  initial: 'idle',
  context: { userId: '42', user: null, error: null },
  states: {
    idle: { on: { FETCH: { target: 'loading' } } },
    loading: {
      invoke: {
        id: 'getUser',
        src: 'fetchUser',
        input: ({ context: { userId } }) => ({ userId }),
        onDone: {
          target: 'success',
          actions: assign({ user: ({ event }) => event.output }),
        },
        onError: {
          target: 'failure',
          actions: assign({ error: ({ event }) => event.error }),
        },
      },
    },
    success: {},
    failure: { on: { RETRY: { target: 'loading' } } },
  },
});

xstate registers a host_actor under the same name. user_id_of, assign_user and assign_error, the functions the config names, are the program’s own, shown in Host actors:

xstate::implementations implementations;
// The promise is the host's work: a host actor.
implementations.actors.emplace("fetchUser", xstate::host_actor{});
implementations.inputs.emplace("getUser", user_id_of);
implementations.actions.emplace("assignUser", xstate::assign_action{.assignment = assign_user});
implementations.actions.emplace("assignError",
                                xstate::assign_action{.assignment = assign_error});
const boost::json::value config = boost::json::parse(R"({
    "id": "user",
    "initial": "idle",
    "context": {"userId": "42", "user": null, "error": null},
    "states": {
        "idle": {"on": {"FETCH": {"target": "loading"}}},
        "loading": {
            "invoke": {
                "id": "getUser",
                "src": "fetchUser",
                "onDone": {"target": "success", "actions": "assignUser"},
                "onError": {"target": "failure", "actions": "assignError"}
            }
        },
        "success": {},
        "failure": {"on": {"RETRY": {"target": "loading"}}}
    }
})");
const xstate::result<xstate::machine> user_machine =
    xstate::create_machine(config, implementations);
if (!user_machine.has_value()) {
    return 1;
}

The program rejects the first request and resolves the second. With XState it settles the promise, and then waits a turn of the event loop, after which the actor has heard of it; printRequests and print are functions of the program:

const actor = createActor(userMachine);
actor.start();
actor.send({ type: 'FETCH' });
printRequests(); // request fetchUser getUser {"userId":"42"}
requests.shift().reject('Network error');
await new Promise((settled) => setTimeout(settled, 0));
print(actor); // "failure", with the error

actor.send({ type: 'RETRY' });
printRequests(); // a new request
requests.shift().resolve({ name: 'David', location: 'Florida' });
await new Promise((settled) => setTimeout(settled, 0));
print(actor); // "success", with the user

With xstate the host lists the pending requests and answers one. The answer runs the system before the call returns, so nothing waits:

xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> actor = system.create_actor(*user_machine);
if (!actor.has_value()) {
    return 1;
}
if (!system.start(*actor).has_value()) {
    return 1;
}
if (!system.send(*actor, {.type = "FETCH", .payload = {}}).has_value()) {
    return 1;
}
std::vector<xstate::host_request> requests = system.host_requests();
print_requests(requests);  // request fetchUser getUser {"userId":"42"}
if (requests.empty()) {
    return 1;
}
if (!system.reject(requests.front(), "Network error").has_value()) {
    return 1;
}
print(system, *actor);  // "failure", with the error

if (!system.send(*actor, {.type = "RETRY", .payload = {}}).has_value()) {
    return 1;
}
requests = system.host_requests();
print_requests(requests);  // a new request
if (requests.empty()) {
    return 1;
}
const boost::json::object user{{"name", "David"}, {"location", "Florida"}};
if (!system.resolve(requests.front(), user).has_value()) {
    return 1;
}
print(system, *actor);  // "success", with the user

It prints:

request fetchUser getUser {"userId":"42"}
"failure" {"userId":"42","user":null,"error":"Network error"}
request fetchUser getUser {"userId":"42"}
"success" {"userId":"42","user":{"name":"David","location":"Florida"},"error":"Network error"}

The rules:

XState xstate

fetchUser: fromPromise(...) in setup({ actors })

host_actor{} in implementations.actors

The promise function’s input

The request’s input, which an invoke’s input gives, or the function registered under the invoke’s id in implementations.inputs (An input computed from the context and the event)

The promise resolves with a value

system.resolve(request, output)

The promise rejects with an error

system.reject(request, error)

event.output in onDone, event.error in onError

The members output and error of args.event.payload

A promise that settles after its actor has stopped is ignored

An answer to a request no longer pending is refused with invalid_argument, and changes nothing

Exceptions become result

XState reports a failure by throwing. createMachine throws on a config it refuses, and an implementation that throws fails the actor that runs it: the snapshot’s status becomes 'error', and its error is what was thrown. xstate throws nothing, since it builds where exceptions are off (No exception of its own). A call that can fail returns a result<T>, Boost.System’s result, which holds the value or an error_code. An implementation that fails returns one, failure<T>(errc::implementation_failed) or any other error_code, and a machine that fails while it runs settles in a snapshot whose status is error and whose error is that code (Errors returned, not thrown).

The toggle above already tests the result its create_machine returned: where XState’s caller would catch, xstate’s tests has_value() and reads error().

An implementation reports its failure with a result too. Here a counter’s setCount fails on an event that carries no count. XState’s throws:

increment: assign(({ context }) => ({ count: context.count + 1 })),
setCount: assign(({ event }) => {
  if (!Number.isInteger(event.count)) {
    throw new Error('set needs a count');
  }
  return { count: event.count };
}),

xstate’s returns a failure:

xstate::result<boost::json::object> increment(const xstate::action_args& args) {
    const boost::json::object* context = args.context.if_object();
    const boost::json::value* count = context == nullptr ? nullptr : context->if_contains("count");
    if (count == nullptr || !count->is_int64()) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"count", count->get_int64() + 1}};
}

xstate::result<boost::json::object> set_count(const xstate::action_args& args) {
    const boost::json::value* count = args.event.payload.if_contains("count");
    if (count == nullptr || !count->is_int64()) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"count", *count}};
}

The program sends set without a count to a snapshot whose count is 1. XState’s getNextSnapshot would let the error out of the call, so XState’s program takes the step with an actor started from that snapshot, which keeps the error in its own:

const actor = createActor(counter, { snapshot: second });
actor.subscribe({ error: () => {} });
actor.start();
actor.send({ type: 'set' });
const failed = actor.getSnapshot();
console.log(failed.status, JSON.stringify(failed.context));

xstate’s get_next_snapshot returns the failed snapshot itself, as an actor’s would be (Failures are values):

const xstate::snapshot failed =
    xstate::get_next_snapshot(*counter, second, xstate::event{.type = "set", .payload = {}});
std::cout << (failed.status == xstate::status::error ? "error " : "active ")
          << boost::json::serialize(failed.context) << '\n';

It prints:

error {"count":1}

The failed step keeps the context it began from, a count of 1, though increment ran before setCount failed: a macrostep that fails settles with the snapshot it began from.

XState xstate

try { createMachine(config) } catch (error) { ... }

create_machine(config, implementations) returns a result<machine>; the program tests has_value() and reads error()

throw new Error('...') in an implementation

return xstate::failure<T>(xstate::errc::implementation_failed);, or any other error_code

snapshot.status === 'error', snapshot.error

snapshot.status == xstate::status::error, and snapshot.error, an error_code whose message() is its name, such as implementation_failed

actor.subscribe({ error })

No error callback: status_of(actor) says the actor failed (Watching an actor)

The error of xstate.error.actor.<id>: what the child threw

The member error of args.event.payload: the failed child’s error, or the name of its code (Failures are values)

The rules:

  • A result is tested before it is read. * reads the value of a result that holds one, and value() of a failure calls boost::throw_exception, which ends a program built without exceptions (webcpp::xstate::result).

  • The actor layer refuses a call it cannot take, such as one that names no actor, with xactor’s invalid_argument, and changes nothing (Errors as values).

  • Boost.JSON’s accessors that check by throwing, such as at and as_int64, have counterparts that answer instead: if_contains and if_int64 with a pointer, try_at and try_as_int64 with a result (Handle context data).

XState’s names

Each function and type of XState that xstate ports, and its counterpart. Some of XState’s are internal to it, not exported by its package; the table marks them.

XState xstate

createMachine(config), setup({ ... }).createMachine(config)

create_machine(config, implementations)

setup({ actions, actors, guards, delays })

implementations

StateMachine

machine

StateNode, TransitionDefinition, InvokeDefinition

state_node, transition_definition, invoke_definition

machine.getStateNodeById(id)

machine::node_by_id

getStateNodeByPath(node, path) (internal)

machine::node_by_path

getStateNodes(node, value)

None in the interface: the nodes of the snapshot resolve_state returns (The active state nodes)

ExecutableActionObject

action

assign, raise, log

assign_action, raise_action, log_action

sendTo, sendParent, forwardTo, cancel, emit

send_to_action, send_parent_action, forward_to_action, cancel_action, emit_action

spawnChild, stopChild

spawn_child_action, stop_child_action

not, and, or, stateIn

xstate.not, xstate.and, xstate.or and xstate.stateIn in a config’s guard (guard_node)

fromPromise

host_actor

getInitialSnapshot, initialTransition, getInitialMicrosteps

get_initial_snapshot, initial_transition, get_initial_microsteps

transition, getNextSnapshot, getMicrosteps

transition, get_next_snapshot, get_microsteps

getNextTransitions

get_next_transitions

snapshot.can(event)

can

snapshot.status

status

snapshot.matches(value), snapshot.hasTag(tag)

snapshot::matches, snapshot::has_tag

snapshot.getMeta()

get_meta

machine.resolveState({ value, context })

resolve_state

machine.toJSON()

to_json (The definition), and create_machine_from_definition to load it back

matchesState, pathToStateValue

matches_state, path_to_state_value

toStatePath, toStateValue (internal)

to_state_path, to_state_value

SimulatedClock

simulated_clock

createActor(machine, { input, id, systemId })

actor_system::create_actor with actor_options

createDoneActorEvent, createErrorActorEvent (internal)

ending_event

actor.start(), actor.send(event), actor.stop()

actor_system::start, send, stop

actor.getSnapshot(), actor.subscribe(observer)

actor_system::snapshot_of, subscribe

actor.on('*', handler)

actor_system::on_emitted

the inspect option of createActor

actor_system::inspect with inspector

actor.system.get(systemId)

actor_system::get

Appendix B: Where xstate differs from XState

xstate ports XState’s algorithm instead of defining semantics of its own, and XState itself judges the port: every case ported from XState’s tests runs on XState and on xstate, and xstate’s result must equal XState’s, microstep by microstep and actor by actor (How xstate is tested). Where the two differ, the difference is deliberate. This appendix gives every one, with its reason; the rule xstate keeps in its place is among xstate’s guarantees, or, for a rule of xactor that the actor layer keeps, among xactor’s. The differences are grouped by their reason:

Each entry says what XState does, what xstate does, and why. Most show the difference with a C++ program and its twin, the same program written for XState (The XState twins): the C++ program, or the section that shows it, then what the twin printed on XState 5.33.2’s development build, the build XState’s own tests run, then what the C++ program printed. Both ran, and the outputs are theirs: the oracle lane, b2 libs/xstate/test/oracle//oracle, runs every twin again and fails when one no longer prints what is shown here. The twin of each program written for this appendix, a diff_<name>, is shown too, under the C++ program, so that both sides of the difference can be read.

What xstate does not have yet is not a divergence and is not listed here: What is not ported lists most of it, and Events one more part. Where XState awaits waitFor or toPromise, a host of the actor layer reads status_of and snapshot_of once its call returns, since every host call runs the system before it returns. Two of these gaps let a config load and then run otherwise than in XState, so they are named here as well. An invoke’s onSnapshot is read as transitions on xstate.snapshot.<id>, but no child sends its parent that event (XState’s syncSnapshot, in the overview’s list), so no child’s snapshot takes them (Invoking actors). And an event xstate.init or xstate.stop sent from outside selects transitions as any other event does, where XState’s macrostep takes no transition for the first and stops the machine for the second (Events).

What JSON imposes

An xstate machine is the JSON of XState’s createMachine config, read when the program runs (The config). That is what lets a program define a machine at run time, or load one that Stately Studio exported, and it makes a machine data: to_json writes its definition as XState’s machine.toJSON() does, and create_machine_from_definition loads it back, given the implementations and the context, which a definition does not hold; The definition says what else it leaves out, and so when the machine loaded back takes the original’s steps. JSON has no function, no undefined and no reference to an object, so every place where XState’s config holds one of them takes another form in xstate. Appendix A gives the C++ spelling of each (From XState to C++); the entries below give what changes beyond the spelling.

Functions become names

XState

A config holds functions: inline actions and guards, the action creators (assign, raise, sendTo and the others), params, an input or an output computed from the context, a context computed from the input, and logic given as an invoke’s src.

xstate

Every function is an implementation registered by name in implementations, as XState’s setup() registers them, and the config names it (Functions become implementations registered by name). Most keep what XState’s function does. Three cannot, and the config holds another form for each:

XState’s config holds xstate’s config holds instead

an inline action, or a function setup() registers as an action

a custom action: a name actions does not hold, which the core returns and the actor layer hands to on_action, where XState runs the function (Custom actions)

params: ({ event }) => ...

fixed params. The functions a built-in action or a guard is registered with read the event and the context from their action_args; a custom action carries its params alone (Custom actions)

src: someMachine, src: fromPromise(...)

the name of an entry of actors (An invoke’s src)

Why

A function cannot be JSON; a name can. Joining the two by name is how XState’s setup() already separates a machine’s logic from its implementations, so the config keeps XState’s shape.

Higher-order guards

XState

not, and and or are functions that wrap other guards, and stateIn one that tests a state value. JSON cannot hold them, and machine.toJSON() writes the transition without its guard.

xstate

They are JSON objects. The type names the combinator with the prefix XState gives its built-in actions, xstate., and the other members are the properties XState’s functions carry, guards and stateValue:

{"type": "xstate.not", "guards": ["isEmpty"]}
{"type": "xstate.and", "guards": ["isAdult", {"type": "hasRole", "params": {"role": "admin"}}]}
{"type": "xstate.or", "guards": ["a", "b"]}
{"type": "xstate.stateIn", "stateValue": "#light.green"}

to_json writes them where XState writes nothing, so a machine loaded back from its definition keeps them.

Why

JSON holds no function. The four forms are xstate’s own, since XState has no JSON for them.

Here SUBMIT is guarded by xstate.not of isEmpty. The program sends it without a name and with one, then reads the guard the definition writes:

xstate::implementations implementations;
implementations.guards.emplace(
    "isEmpty", [](const xstate::action_args& args) -> xstate::result<bool> {
        const boost::json::value* name = args.event.payload.if_contains("name");
        if (name == nullptr) {
            return true;
        }
        return name->is_string() && name->get_string().empty();
    });
const boost::json::value config = boost::json::parse(R"({
    "id": "form",
    "initial": "editing",
    "states": {
        "editing": {
            "on": {
                "SUBMIT": {
                    "target": "submitted",
                    "guard": {"type": "xstate.not", "guards": ["isEmpty"]}
                }
            }
        },
        "submitted": {}
    }
})");
const xstate::result<xstate::machine> form = xstate::create_machine(config, implementations);
if (!form.has_value()) {
    return 1;
}
const xstate::snapshot editing = xstate::get_initial_snapshot(*form);
const xstate::snapshot unnamed = xstate::get_next_snapshot(
    *form, editing, xstate::event{.type = "SUBMIT", .payload = {{"name", ""}}});
const xstate::snapshot named = xstate::get_next_snapshot(
    *form, editing, xstate::event{.type = "SUBMIT", .payload = {{"name", "Ana"}}});
std::cout << "SUBMIT without a name: " << boost::json::serialize(unnamed.value) << '\n'
          << "SUBMIT with a name: " << boost::json::serialize(named.value) << '\n';

const boost::json::value definition = xstate::to_json(*form);
boost::system::error_code missing;
const boost::json::value* guard =
    definition.find_pointer("/states/editing/on/SUBMIT/0/guard", missing);
std::cout << "the definition's guard: "
          << (guard == nullptr ? "undefined" : boost::json::serialize(*guard)) << '\n';
The twin, on XState
import { getInitialSnapshot, getNextSnapshot, not, setup } from 'xstate';

const form = setup({
  guards: {
    isEmpty: ({ event }) => !event.name,
  },
}).createMachine({
  id: 'form',
  initial: 'editing',
  states: {
    editing: {
      on: {
        SUBMIT: { target: 'submitted', guard: not('isEmpty') },
      },
    },
    submitted: {},
  },
});

const editing = getInitialSnapshot(form);
const unnamed = getNextSnapshot(form, editing, { type: 'SUBMIT', name: '' });
const named = getNextSnapshot(form, editing, { type: 'SUBMIT', name: 'Ana' });
console.log(`SUBMIT without a name: ${JSON.stringify(unnamed.value)}`);
console.log(`SUBMIT with a name: ${JSON.stringify(named.value)}`);

// The definition as JSON, as a file would hold it.
const definition = JSON.parse(JSON.stringify(form.toJSON()));
const guard = definition.states.editing.on.SUBMIT[0].guard;
console.log(`the definition's guard: ${JSON.stringify(guard)}`);
XState prints
SUBMIT without a name: "editing"
SUBMIT with a name: "submitted"
the definition's guard: undefined
xstate prints
SUBMIT without a name: "editing"
SUBMIT with a name: "submitted"
the definition's guard: {"type":"xstate.not","guards":["isEmpty"]}

The context function

XState

context: ({ input, spawn, self }) => ... receives the input, a spawn function and the actor itself, and a context may hold an actor it spawned.

xstate

implementations.context receives the input only (Initial context from the input). A context is JSON and holds no actor; a child is spawned by an invoke or a spawn_child_action.

Why

A reference to an actor is not data (Targets that name actors), and the machine core runs no actor: it includes nothing of xactor, holds a machine’s actors and children as data, and resolves a spawn into an xstate.spawnChild action for its caller to run, which the actor layer does (Spawning actors).

An invoke’s src

XState

src is the name of an actor setup() registered, or the logic itself: a machine, or fromPromise(...).

xstate

src is a string that names an entry of implementations.actors. Any other src refuses the machine with invalid_config, and a name with no entry with unknown_actor (A config is refused when it is created). A host_actor stands for fromPromise: the host answers it (Host actors).

Why

Logic cannot be JSON.

Targets that name actors

XState

sendTo takes an actor reference, a string (a child’s id, #_parent, #_internal, #_<id>) or a function that returns one; a send to a systemId is written sendTo(({ system }) => system.get('notifier'), ...). An event or an input may carry an actor reference, as a parent passes self to a child that must answer it.

xstate

A target is a string: a child’s id or #_<id>, #_parent, #_internal, or #system:<systemId>, the actor registered under that systemId, which the actor layer resolves when the action resolves. When no actor holds the systemId, a sendTo goes to the actor itself, as XState’s does when the function returns undefined, and a forwardTo fails (The checks of XState’s development build). Events and inputs carry no actor: a child answers its parent through #_parent, and any other actor is named by its id or its systemId.

Why

JSON holds neither a reference nor a function.

Custom actions

XState

An actor runs each action’s implementation, a function, when the action executes. The function receives the context, the event, the actor and its system, and the action’s params, which may themselves be computed from the context and the event.

xstate

An action that implementations.actions does not hold is a custom action (Custom actions). The machine core returns it as {type, params}, as XState’s pure transition() returns the actions to execute, and the actor layer hands each one, in order, to the callback on_action registers, with the actor that returned it, and nothing else; a log reaches on_log the same way (Watching an actor). Where XState’s function receives the context and the event, the callback receives neither. What a custom action needs from them must be in its fixed params, or reach the host through an emit_action, whose event_maker reads both.

Why

A function cannot be JSON, and what a machine does to the world belongs to the program that runs it.

undefined

XState

JavaScript tells undefined from null. The event xstate.init always has a member input, undefined when the actor was given none, and an implementation that returns undefined leaves a log without a value, a child without an input or a snapshot without an output.

xstate

JSON has null only. A value_maker, the implementation that computes a log’s value, an input or an output, returns a result<std::optional<boost::json::value>>, and none, the empty optional, stands for undefined: the member is left out, as XState leaves it out (When a value is undefined). xstate.init carries input only when the input is not null, so a null input and no input are the same.

Why

The JSON xstate reads and writes cannot hold undefined, and a member left out is how XState itself writes one.

A state’s null meta, and a description that is not a string

XState

A state’s meta that is null is kept: getMeta() lists it, and machine.toJSON() writes it. A state’s description is kept whatever its type, and written too, although XState’s own type declarations make it a string.

xstate

A null meta is no meta: get_meta does not list it, and to_json leaves it out. A description that is not a string is left out of the machine, and so of its definition (What creation refuses). A transition’s meta and description are kept as XState keeps them.

Why

A null is how JSON writes no value, so it stands for no meta, as it stands for no context (A context that is not an object). A state’s description is text, as XState’s declarations make it, and state_node::description holds it as a string; a transition’s is kept as the JSON the config gives.

What the actor model on xactor imposes

xstate’s actor layer runs machines as XState’s actors on xactor, webcpp’s actor system (xactor’s model): one scheduler, which holds a first-in first-out mailbox for each actor and a first-in first-out queue of the actors that have work, and hands one message to its actor per turn; fuel paid for every microstep, every message an actor sends and every timer it arms; timers owned by actors; and time the host brings. That makes a run deterministic, bounds every call the host makes, and reuses xactor’s scheduler, fuel and timers instead of building a second actor system beside it (How the layer runs). Within one actor, everything happens in XState’s order. Between actors, and around the host’s calls, the entries below differ.

Entries of the other groups concern the actor layer too. Of JSON’s: custom actions handed to on_action (Custom actions), a host actor in place of a promise (An invoke’s src) and targets named by strings (Targets that name actors). Of XState’s defects: a root that sends to its parent (sendParent without a parent), an emitted event of the type * (An emitted event of the type *), the children of a failing macrostep (The children of a failing macrostep), the timers of an ended actor (Timers of an ended actor), a timer that falls due during clock_tick (A timer that falls due during clock_tick), the host’s stop of a family with a failed member (The host’s stop of a family with a failed member) and a sendTo whose child is gone (A sendTo whose child is gone). Of the API’s: a failed actor’s error and the error event it sends its parent (Failures are values), and the listeners of emitted events (Listeners of emitted events).

Interleaving across actors

XState

An event sent to an idle actor is delivered at once, nested in the sender’s call stack. start() starts an actor’s children first, each running its initial actions, and then runs the actor’s own.

xstate

xactor queues every delivery, the message in its actor’s mailbox and the actor in the queue of actors with work, and a turn hands one message to its actor, so a delivery never runs inside its sender’s call. Each actor receives what one sender sends it in the order sent, and handles it in XState’s order; but when an actor hears from two senders and one send is caused by the other’s, the order between the two may differ, and so may the order of callbacks across actors. So an actor’s start hands over its own initial custom actions before its initial children handle theirs. In the same way, a parent handles the last sends of a child that is done once the child has released its systemId, where XState, delivering them nested, has not released it yet; and a child born done or failed, started by its parent, finds the systemIds as the parent’s macrostep left them, where in XState a sibling may have ended, or an actor a nested send reached may have claimed one, before that start.

Why

Queues instead of a call stack make a run deterministic and replayable, keep one actor’s work from nesting inside another’s, and let fuel bound each turn. The tests compare each actor’s own sequence, never the order across actors (XState as the oracle).

Here an app whose entry is the custom action appStarted invokes a kid whose entry is kidStarted, and the host prints each custom action it is handed:

const boost::json::value kid_config = boost::json::parse(R"({
    "entry": "kidStarted"
})");
const xstate::result<xstate::machine> kid = xstate::create_machine(kid_config, {});
if (!kid.has_value()) {
    return 1;
}
xstate::implementations implementations;
implementations.actors.emplace("kid", xstate::machine_actor{*kid});
const boost::json::value app_config = boost::json::parse(R"({
    "entry": "appStarted",
    "invoke": {"id": "kid", "src": "kid"}
})");
const xstate::result<xstate::machine> app = xstate::create_machine(app_config, implementations);
if (!app.has_value()) {
    return 1;
}
xstate::actor_system system({.fuel = 10'000});
const xstate::result<void> acting = system.on_action(
    [](xstate::actor_ref, const xstate::action& custom) { std::cout << custom.type << '\n'; });
if (!acting.has_value()) {
    return 1;
}
const xstate::result<xstate::actor_ref> actor = system.create_actor(*app);
if (!actor.has_value()) {
    return 1;
}
if (!system.start(*actor).has_value()) {
    return 1;
}
The twin, on XState
import { createActor, setup } from 'xstate';

const kid = setup({
  actions: { kidStarted: () => console.log('kidStarted') },
}).createMachine({
  entry: 'kidStarted',
});
const app = setup({
  actors: { kid },
  actions: { appStarted: () => console.log('appStarted') },
}).createMachine({
  entry: 'appStarted',
  invoke: { id: 'kid', src: 'kid' },
});

createActor(app).start();
XState prints
kidStarted
appStarted
xstate prints
appStarted
kidStarted

Fuel

XState

Nothing bounds a call unless the config asks for it: createActor, start() and send() return once every macrostep they run has settled, so one that reaches an actor whose eventless transitions never settle never returns. For a root whose initial macrostep never settles, that call is createActor, which runs the macrostep (The root is constructed on start). The bound XState has is the config’s options.maxIterations: once that many microsteps have followed the event’s own with the machine still active, the macrostep throws an error whose message begins Infinite loop detected, even when it would settle there. transition lets the error out, and an actor catches it into its snapshot, whose status is then 'error'.

xstate

Every microstep costs one unit of the execution’s fuel, as does every message an actor sends and every timer it arms. When an execution runs out, the actor parks and the call returns out_of_fuel; resume() gives every parked actor a new execution. While an actor is parked, the host’s start, send, clock_tick, resolve and reject are held, in order, and applied by a later resume(), so no event starts before the macrostep in progress has settled. stop is never held: it is how the host ends an actor that would never settle. xstate does not read options.

Why

The program that runs a machine decides how much work an event may cost, from outside, one microstep at a time, so a machine loaded at run time cannot hang it (Fuel, parking and resume). A bound written in the config is its author’s, and ends the macrostep as a failure; fuel is the host’s, and a parked actor resumes where it stopped. The core’s pure functions, get_next_snapshot among them, run without a bound, as XState’s do when the config sets none; a caller that must bound the work drives the macrostep cursor itself, one microstep per next() (the cursor’s fuel).

Here a root’s initial macrostep never settles: ping and pong lead to each other. Each execution has 100 units of fuel. The twin gives its config an options.maxIterations of 1000, without which its createActor never returns; with it, createActor throws inside and leaves the actor in error before start():

const boost::json::value config = boost::json::parse(R"({
    "initial": "ping",
    "states": {
        "ping": {"always": "pong"},
        "pong": {"always": "ping"}
    }
})");
const xstate::result<xstate::machine> ping_pong = xstate::create_machine(config, {});
if (!ping_pong.has_value()) {
    return 1;
}
xstate::actor_system system({.fuel = 100});
const xstate::result<xstate::actor_ref> actor = system.create_actor(*ping_pong);
if (!actor.has_value()) {
    return 1;
}
const xstate::result<xstate::run_outcome> started = system.start(*actor);
if (!started.has_value()) {
    return 1;
}
std::cout << "start(): " << outcome_name(*started) << '\n';
const xstate::result<xstate::run_outcome> resumed = system.resume();
if (!resumed.has_value()) {
    return 1;
}
std::cout << "resume(): " << outcome_name(*resumed) << '\n';
if (!system.stop(*actor).has_value()) {
    return 1;
}
const xstate::result<webcpp::xactor::status> ended = system.status_of(*actor);
if (!ended.has_value()) {
    return 1;
}
std::cout << "stop(): " << (*ended == webcpp::xactor::status::stopped ? "stopped" : "running")
          << '\n';
The twin, on XState
import { createActor, createMachine } from 'xstate';

const pingPong = createMachine({
  initial: 'ping',
  // Without it, createActor never returns: it runs the initial macrostep.
  options: { maxIterations: 1000 },
  states: {
    ping: { always: 'pong' },
    pong: { always: 'ping' },
  },
});

const actor = createActor(pingPong);
actor.subscribe({ error: () => {} });
actor.start();
console.log('start(): ' + actor.getSnapshot().status);
actor.stop();
console.log('stop(): ' + actor.getSnapshot().status);
XState prints
start(): error
stop(): error
xstate prints
start(): out_of_fuel
resume(): out_of_fuel
stop(): stopped

The start and the resume each run as many microsteps as their execution pays for, and the actor parks again; the host’s stop ends it. XState’s actor failed in createActor, and stays in error after stop().

The core shows the same two bounds. In the cursor’s example of a cycle, the caller gives SPIN 1000 units of fuel, one per next() of the cursor, where the twin gives the config an options.maxIterations of 1000:

XState prints
transition throws: Infinite loop detected
the stable state is still "idle"
xstate prints
out of fuel after 1000 microsteps, at "right"
the stable state is still "idle"

And in the example of an eventless cycle, the caller pays for five microsteps, printing each, where the twin gives the config an options.maxIterations of 5:

XState prints
transition throws: Infinite loop detected
xstate prints
"ping"
"pong"
"ping"
"pong"
"ping"
not settled after 5 microsteps

XState’s transition throws and returns no snapshot of the cycle; its caller keeps the snapshot it passed. xstate’s caller holds the cursor, a value: the snapshot inside the cycle, the stable one it began from, and the choice to drop the event, report it, or resume with more fuel.

The root is constructed on start

XState

createActor constructs the root at once: its initial macrostep runs, getSnapshot() returns its snapshot, and its system holds the systemIds of its initial children, all before start().

xstate

create_actor creates the actor and runs nothing. Its construction runs on start, so until then snapshot_of returns nullptr and get finds none of its children’s systemIds; its initial children also take their session numbers then (One system per actor_system).

Why

Construction runs a macrostep, which costs fuel, and fuel is paid inside an execution, which only a host call that delivers something opens (start, send, clock_tick, resolve, reject, resume). create_actor delivers nothing.

Here the root invokes a worker registered under the systemId worker, and describe prints the root’s state value and whether worker is registered, before the start and after it:

/** Prints the actor's state value, if it has a snapshot, and whether "worker" is registered. */
void describe(std::string_view moment, const xstate::actor_system& system, xstate::actor_ref app) {
    const xstate::snapshot* now = system.snapshot_of(app);
    const std::string value = now == nullptr ? "no snapshot" : boost::json::serialize(now->value);
    const std::string_view worker = system.get("worker").has_value() ? "registered" : "unknown";
    std::cout << moment << ": " << value << ", worker " << worker << '\n';
}
const boost::json::value worker_config = boost::json::parse("{}");
const xstate::result<xstate::machine> worker = xstate::create_machine(worker_config, {});
if (!worker.has_value()) {
    return 1;
}
xstate::implementations implementations;
implementations.actors.emplace("worker", xstate::machine_actor{*worker});
const boost::json::value config = boost::json::parse(R"({
    "initial": "running",
    "states": {
        "running": {"invoke": {"id": "w", "src": "worker", "systemId": "worker"}}
    }
})");
const xstate::result<xstate::machine> app = xstate::create_machine(config, implementations);
if (!app.has_value()) {
    return 1;
}
xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> actor = system.create_actor(*app);
if (!actor.has_value()) {
    return 1;
}
describe("before start", system, *actor);
if (!system.start(*actor).has_value()) {
    return 1;
}
describe("after start", system, *actor);
The twin, on XState
import { createActor, createMachine, setup } from 'xstate';

const worker = createMachine({});
const app = setup({ actors: { worker } }).createMachine({
  initial: 'running',
  states: {
    running: { invoke: { id: 'w', src: 'worker', systemId: 'worker' } },
  },
});

function describe(moment, actor) {
  const registered = actor.system.get('worker') !== undefined ? 'registered' : 'unknown';
  const value = JSON.stringify(actor.getSnapshot().value);
  console.log(`${moment}: ${value}, worker ${registered}`);
}

const actor = createActor(app);
describe('before start', actor);
actor.start();
describe('after start', actor);
XState prints
before start: "running", worker registered
after start: "running", worker registered
xstate prints
before start: no snapshot, worker unknown
after start: "running", worker registered

One system per actor_system

XState

createActor makes a system of its own for every root: its systemIds, system.get and the sends to a systemId. An actor’s session id, its id when it is given none, is x:<n>, where n counts every actor the process created, across all its systems.

xstate

The roots of one actor_system share one system. A systemId names one actor across all of them: a child of one root is refused a systemId that another root’s family holds, and create_actor refuses one too, with system_id_taken. A root’s session id counts the actors its own actor_system created before it, a spawn whose claim of a systemId was refused included, so the two agree when a process runs one actor_system with one root. With fuel, a root created while another is parked takes its number before that root’s children are all created. A program that wants XState’s arrangement gives each root an actor_system of its own (The system).

Why

An actor_system owns one xactor scheduler, with its registry of systemIds and its counter of actors. Nothing of it is global: xstate keeps no global or static mutable state.

Time in whole milliseconds

XState

SimulatedClock and the delays take fractions of a millisecond. An after key that JavaScript reads as a number is that many milliseconds, "1.5", "-1" and "1e3" among them, and any other key names a delay, "2fast" among them.

xstate

xactor’s clock, which clock_tick moves, and the core’s simulated_clock count whole milliseconds, and a delay is a std::uint64_t. An after key is a number of milliseconds only when it is digits alone. Any other key that starts with a digit, a sign or a dot, such as "1.5", "-1", "1e3" or "2fast", refuses the machine with invalid_config, even when a delay of that name is registered, and the remaining keys name delays.

Why

xactor’s timers count whole milliseconds (xactor’s timers), which keeps every deadline an exact integer; a fraction has no timer to hold it, and a negative delay has no meaning. A key is read as a number only when it is digits alone, as every such key is a whole number of milliseconds.

A tick earlier than the last

XState

SimulatedClock.set throws for a time before the current one, and the time stays where it was.

xstate

clock_tick accepts it: the time moves back, the timers due by then are released, and a timer armed afterwards counts from the earlier time, so it can fall due at a time the clock has already passed once. The core’s simulated_clock refuses the same move (The core’s clock hands over the due events). A host that wants XState’s behaviour passes a time that never decreases.

Why

The actor layer’s time is xactor’s, the time the host brings: xactor reads no clock, and its rule is that a tick earlier than the last moves now back (xactor’s guarantee 18 and guarantee 34). The actor layer, whose timers are xactor’s, keeps that rule as it is. The core’s simulated_clock is a port of XState’s SimulatedClock, and keeps its refusal.

Here the time goes to 1000 and back to 10, and then an actor enters a state that leaves after 100 ms:

const boost::json::value config = boost::json::parse(R"({
    "initial": "green",
    "states": {
        "green": {"after": {"100": "yellow"}},
        "yellow": {}
    }
})");
const xstate::result<xstate::machine> light = xstate::create_machine(config, {});
if (!light.has_value()) {
    return 1;
}
xstate::actor_system system({.fuel = 10'000});
if (!system.clock_tick(1000).has_value()) {
    return 1;
}
const xstate::result<xstate::run_outcome> back = system.clock_tick(10);
if (!back.has_value()) {
    return 1;
}
std::cout << "clock_tick(10): " << outcome_name(*back) << '\n';
const xstate::result<xstate::actor_ref> actor = system.create_actor(*light);
if (!actor.has_value()) {
    return 1;
}
// The timer of green is armed now, 100 ms from 10.
if (!system.start(*actor).has_value()) {
    return 1;
}
for (const std::uint64_t time : {1050, 1100}) {
    if (!system.clock_tick(time).has_value()) {
        return 1;
    }
    const xstate::snapshot* now = system.snapshot_of(*actor);
    if (now == nullptr) {
        return 1;
    }
    std::cout << "at " << time << ": " << boost::json::serialize(now->value) << '\n';
}
The twin, on XState
import { SimulatedClock, createActor, createMachine } from 'xstate';

const light = createMachine({
  initial: 'green',
  states: {
    green: { after: { 100: 'yellow' } },
    yellow: {},
  },
});

const clock = new SimulatedClock();
clock.set(1000);
try {
  clock.set(10);
  console.log(`set(10): the time is ${clock.now()}`);
} catch (error) {
  console.log(`set(10) throws: ${error.message}`);
}
const actor = createActor(light, { clock });
actor.start();
for (const time of [1050, 1100]) {
  clock.set(time);
  console.log(`at ${time}: ${JSON.stringify(actor.getSnapshot().value)}`);
}
XState prints
set(10) throws: Unable to travel back in time
at 1050: "green"
at 1100: "yellow"
xstate prints
clock_tick(10): settled
at 1050: "yellow"
at 1100: "yellow"

XState’s timer counts from 1000 and fires at 1100; xstate’s counts from 10, so the tick to 1050 releases it.

A callback that calls the system back

XState

A subscriber, an actor.on listener or an inspector may call the system: a send from one of them is accepted, and the actor handles the event once the update in progress is done.

xstate

The host’s callbacks, a subscribe or on_emitted listener, on_action, on_log and the inspector, run inside an actor’s turn, while the system runs. A call from one of them to a member of actor_system that changes the system, create_actor, start, send, clock_tick, resolve, reject, resume, run_until_idle, stop, or a registration (subscribe, on_emitted, on_action, on_log, inspect), returns xactor’s errc::invalid_argument, of the category webcpp.xactor, and changes nothing. The const members, which read the system, snapshot_of, status_of, get and the others, are free. A host that must act on what it heard records it in the callback and acts once the call that ran the system has returned (Watching an actor).

Why

Every host call that delivers runs the system and returns the outcome of that run, settled or parked. A call made inside the run cannot know that outcome, and running the system again from inside a turn would nest one run in another, where xactor runs one turn at a time, each to its end.

Here a listener prints each snapshot it hears, and sends NEXT when it hears "b"; print_value prints the state value after each of the host’s own sends:

/** Prints the state value an actor's snapshot holds, after `moment`. */
void print_value(const xstate::actor_system& system, xstate::actor_ref actor,
                 std::string_view moment) {
    const xstate::snapshot* now = system.snapshot_of(actor);
    if (now == nullptr) {
        return;
    }
    std::cout << moment << ": " << boost::json::serialize(now->value) << '\n';
}
const boost::json::value config = boost::json::parse(R"({
    "initial": "a",
    "states": {
        "a": {"on": {"NEXT": "b"}},
        "b": {"on": {"NEXT": "c"}},
        "c": {}
    }
})");
const xstate::result<xstate::machine> steps = xstate::create_machine(config, {});
if (!steps.has_value()) {
    return 1;
}
xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> actor = system.create_actor(*steps);
if (!actor.has_value()) {
    return 1;
}
const xstate::actor_ref stepper = *actor;
bool heard_b = false;
const xstate::result<void> subscribed =
    system.subscribe(stepper, [&system, &heard_b, stepper](const xstate::snapshot& heard) {
        std::cout << "heard " << boost::json::serialize(heard.value) << '\n';
        if (!heard.matches("b")) {
            return;
        }
        heard_b = true;
        const xstate::result<xstate::run_outcome> sent =
            system.send(stepper, {.type = "NEXT", .payload = {}});
        if (!sent.has_value()) {
            const boost::system::error_code& refused = sent.error();
            std::cout << "  the listener's send: " << refused.message() << ", "
                      << refused.category().name() << '\n';
        }
    });
if (!subscribed.has_value()) {
    return 1;
}
if (!system.start(stepper).has_value()) {
    return 1;
}
if (!system.send(stepper, {.type = "NEXT", .payload = {}}).has_value()) {
    return 1;
}
print_value(system, stepper, "after the host's send");

// The host acts on what the listener recorded once its own call has returned.
if (heard_b && !system.send(stepper, {.type = "NEXT", .payload = {}}).has_value()) {
    return 1;
}
print_value(system, stepper, "after the host's second send");
The twin, on XState
import { createActor, createMachine } from 'xstate';

const steps = createMachine({
  initial: 'a',
  states: {
    a: { on: { NEXT: 'b' } },
    b: { on: { NEXT: 'c' } },
    c: {},
  },
});

const actor = createActor(steps);
actor.subscribe((snapshot) => {
  console.log(`heard ${JSON.stringify(snapshot.value)}`);
  if (snapshot.value === 'b') {
    actor.send({ type: 'NEXT' });
    console.log('  the listener sent NEXT');
  }
});
actor.start();
actor.send({ type: 'NEXT' });
console.log(`after the host's send: ${JSON.stringify(actor.getSnapshot().value)}`);
XState prints
heard "a"
heard "b"
  the listener sent NEXT
heard "c"
after the host's send: "c"
xstate prints
heard "a"
heard "b"
  the listener's send: invalid_argument, webcpp.xactor
after the host's send: "b"
heard "c"
after the host's second send: "c"

Where XState leaks JavaScript or has a defect

Some of what XState does is an accident: of JavaScript, of V8, or of an implementation that XState’s own tests do not reach. xstate does not reproduce the accident: it does what the accident gets in the way of, or states as a rule what XState gets by chance.

sendParent without a parent

XState

The pure functions run a machine as an inert actor, which has no parent, so transition() throws on a sendParent, which a running child resolves to its parent.

xstate

The machine core returns it as XState returns it for an actor that has a parent: xstate.sendTo to #_parent. Delivering it is the caller’s work. In the actor layer, a root that sends to its parent fails with unknown_target, as a root actor of XState fails.

Why

Whether a machine has a parent depends on where it runs, not on the machine, and the pure functions describe the machine.

Here FINISH takes a sendParent of DONE, and the program prints each action transition returns:

xstate::implementations implementations;
implementations.actions.emplace(
    "reportDone", xstate::send_parent_action{
                      .event = [](const xstate::action_args&) -> xstate::result<xstate::event> {
                          return xstate::event{.type = "DONE", .payload = {}};
                      },
                      .id = std::nullopt,
                      .delay = std::nullopt,
                  });
const boost::json::value config = boost::json::parse(R"({
    "id": "child",
    "on": {"FINISH": {"actions": "reportDone"}}
})");
const xstate::result<xstate::machine> child = xstate::create_machine(config, implementations);
if (!child.has_value()) {
    return 1;
}
const auto [next, actions] = xstate::transition(*child, xstate::get_initial_snapshot(*child),
                                                xstate::event{.type = "FINISH", .payload = {}});
for (const xstate::action& returned : actions) {
    std::cout << returned.type << ' ' << boost::json::serialize(returned.params) << '\n';
}
The twin, on XState
import { createMachine, getInitialSnapshot, sendParent, transition } from 'xstate';

const child = createMachine({
  id: 'child',
  on: { FINISH: { actions: sendParent({ type: 'DONE' }) } },
});

try {
  const [next, actions] = transition(child, getInitialSnapshot(child), { type: 'FINISH' });
  for (const returned of actions) {
    console.log(returned.type, JSON.stringify(returned.params));
  }
} catch (error) {
  console.log(`transition() throws: ${error.message}`);
}
XState prints
transition() throws: Unable to send event to actor '#_parent' from machine 'child'.
xstate prints
xstate.sendTo {"targetId":"#_parent","event":{"type":"DONE"}}

An emitted event of the type *

XState

emit hands an event to the listeners of its type and to the listeners of *. For an event whose type is *, the two lookups find the same listeners, and each hears the event twice.

xstate

on_emitted hears every event an actor emits, as XState’s actor.on('*') does, and an event of the type * reaches each listener once.

Why

A listener hears an event once; hearing it twice is a defect of the lookup.

Here PING emits an event of the type * to one listener:

xstate::implementations implementations;
implementations.actions.emplace(
    "emitStar", xstate::emit_action{
                    .event = [](const xstate::action_args&) -> xstate::result<xstate::event> {
                        return xstate::event{.type = "*", .payload = {}};
                    },
                });
const boost::json::value config = boost::json::parse(R"({
    "on": {"PING": {"actions": "emitStar"}}
})");
const xstate::result<xstate::machine> emitter = xstate::create_machine(config, implementations);
if (!emitter.has_value()) {
    return 1;
}
xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> actor = system.create_actor(*emitter);
if (!actor.has_value()) {
    return 1;
}
const xstate::result<void> listening = system.on_emitted(
    *actor, [](const xstate::event& heard) { std::cout << "heard " << heard.type << '\n'; });
if (!listening.has_value()) {
    return 1;
}
if (!system.start(*actor).has_value()) {
    return 1;
}
if (!system.send(*actor, xstate::event{.type = "PING", .payload = {}}).has_value()) {
    return 1;
}
The twin, on XState
import { createActor, emit, setup } from 'xstate';

const emitter = setup({
  actions: {
    emitStar: emit({ type: '*' }),
  },
}).createMachine({
  on: { PING: { actions: 'emitStar' } },
});

const actor = createActor(emitter);
actor.on('*', (heard) => console.log(`heard ${heard.type}`));
actor.start();
actor.send({ type: 'PING' });
XState prints
heard *
heard *
xstate prints
heard *

A spawned child without an id

XState

spawnChild(src) without an id keys the child "undefined", JavaScript’s spelling of the missing value, and a stopChild of it then leaves that key in the snapshot’s children.

xstate

The id of a spawn_child_action is fixed or computed by an id_maker, and one left out is the empty string: the child is keyed under "", and a stop_child_action of "" removes it from the snapshot’s children.

Why

A C++ string has no undefined: an id left out holds the empty string, a key a stop can name.

In both, every child spawned without an id has the same key, as does a child given that key as its id ("" here, "undefined" in XState). A second such spawn takes the key from the first child, which keeps running, but its parent can no longer name it, to send it an event or to stop it. Give each spawned child an id of its own, or a system_id to find it by.

Here two children are spawned without an id, each with a system_id, and the key names the second:

const boost::json::value child_config = boost::json::parse(R"({"id": "child"})");
const xstate::result<xstate::machine> child_machine = xstate::create_machine(child_config, {});
if (!child_machine.has_value()) {
    return 1;
}
xstate::implementations implementations;
implementations.actors.emplace("childMachine", xstate::machine_actor{*child_machine});
implementations.actions.emplace("spawnFirst", spawning("first"));
implementations.actions.emplace("spawnSecond", spawning("second"));
const boost::json::value config = boost::json::parse(R"({
    "entry": ["spawnFirst", "spawnSecond"]
})");
const xstate::result<xstate::machine> parent_machine =
    xstate::create_machine(config, implementations);
if (!parent_machine.has_value()) {
    return 1;
}

xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> parent = system.create_actor(*parent_machine);
if (!parent.has_value() || !system.start(*parent).has_value()) {
    return 1;
}
const xstate::snapshot* current = system.snapshot_of(*parent);
if (current == nullptr) {
    return 1;
}
boost::json::array keys;
for (const auto& [id, src] : current->children) {
    keys.emplace_back(id);
}
std::cout << boost::json::serialize(keys) << '\n';
const std::optional<xstate::actor_ref> keyed = system.child_of(*parent, "");
for (const std::string_view name : {"first", "second"}) {
    const std::optional<xstate::actor_ref> child = system.get(name);
    if (!child.has_value()) {
        return 1;
    }
    const xstate::result<xactor::status> status = system.status_of(*child);
    if (!status.has_value()) {
        return 1;
    }
    std::cout << name << ": " << name_of(*status)
              << ", keyed: " << (keyed == child ? "true" : "false") << '\n';
}
The twin, on XState
import { createActor, createMachine, setup, spawnChild } from 'xstate';

const childMachine = createMachine({ id: 'child' });

const parentMachine = setup({
  actors: { childMachine },
  actions: {
    spawnFirst: spawnChild('childMachine', { systemId: 'first' }),
    spawnSecond: spawnChild('childMachine', { systemId: 'second' }),
  },
}).createMachine({
  entry: ['spawnFirst', 'spawnSecond'],
});

const parent = createActor(parentMachine);
parent.start();
const children = parent.getSnapshot().children;
console.log(JSON.stringify(Object.keys(children)));
for (const name of ['first', 'second']) {
  const child = parent.system.get(name);
  const keyed = Object.values(children).includes(child);
  console.log(`${name}: ${child.getSnapshot().status}, keyed: ${keyed}`);
}
XState prints
["undefined"]
first: active, keyed: false
second: active, keyed: true
xstate prints
[""]
first: active, keyed: false
second: active, keyed: true

Equal deadlines

XState

SimulatedClock sorts its timers with a comparator that never returns 0, so the order of two timers with the same deadline is whatever V8’s sort makes of it: the order they were scheduled in.

xstate

The order is a rule: by deadline, then by the order the timers were scheduled in, in the core’s simulated_clock and in the actor layer’s clock_tick.

Why

The result is XState’s; xstate states it instead of leaving it to a sort.

The children of a failing macrostep

XState

When a macrostep fails after it spawned children, because an implementation throws or a later action claims a systemId another actor holds, the children it spawned stay constructed and are never started, their systemIds still registered.

xstate

The actor fails, and the children the macrostep created and did not start are stopped, which releases their systemIds. When the actor falls back to the snapshot the macrostep began from, the started children it had asked to stop are its own again, as that snapshot lists them.

Why

A child that is never started and never stopped would hold its systemId for good, and no later actor could claim the name.

Timers of an ended actor

XState

When an actor stops, is done or fails, its cancelAll cancels the timers its _scheduledEvents lists: one per id, the latest armed under it, until a timer of that id fires and removes the entry. So of two timers armed under one id, the earlier still fires, or the later once the earlier has fired, and its event is delivered when its target still runs.

xstate

xactor drops every timer of an actor that ends.

Why

An actor that has ended sends nothing.

A timer that falls due during clock_tick

XState

SimulatedClock fires a timer armed during a tick, for a deadline the tick has already reached, only while its flush has timers left to visit.

xstate

clock_tick(now) releases the timers due by now one at a time, in the order of their deadlines, and a timer armed meanwhile and due by now is released in the same call.

Why

Whether a due timer fires cannot depend on how many timers the flush had left to visit.

The host’s stop of a family with a failed member

XState

actor.stop() stops the root actor, its system and, in the words of XState’s documentation, the actors in that system. It reaches a child only through a running parent, though, so the children of a failed actor keep running.

xstate

system.stop(root) stops the whole family at once, the children of a failed member included. A parent’s stopChild of a failed child keeps that child’s children running, as XState does.

Why

The host’s stop is how a program ends a family of actors; one that leaves some of them running does not end it.

A sendTo whose child is gone

XState

A sendTo that names an invoke of the state being entered is bound to its child once the state’s entry, spawns and initial actions are resolved (retryResolveSendTo). When no child has that id at that point, XState relays the event to undefined, which throws a TypeError.

xstate

The actor fails with unknown_target when the send is delivered.

Why

A send to an actor that is not there is an error of the machine, and xstate reports it as one, with the code it reports for every other target that names no actor.

A context that is not an object

XState

A context is a JavaScript object. A falsy context in the config, null, 0, false or "", starts the machine with {}, and any other is kept. An assign merges with Object.assign, which spreads a string or an array into numbered members, so "ab" becomes {"0": "a", "1": "b"}, and leaves out a number or true; whatever a context function returns is merged into an empty object the same way.

xstate

A config whose context is null, or that has none, starts the machine with {}, as XState’s falsy context does. Any other value that is not an object, false, a number, a string or a list, is kept as written, and an assign on it fails with implementation_failed, where XState starts the machine with {} for a falsy one and merges the update into any other with Object.assign (Initial context). A context function, implementations.context, returns a result<boost::json::value>: a value that is not an object fails the initial macrostep with implementation_failed, and a failure it returns fails it with its own code (Initial context from the input).

Why

Truthiness and the spreading of a string are properties of JavaScript. A JSON machine whose context is not an object has a fault, and xstate reports it where it shows instead of turning the value into another. A null is how JSON writes no value, so it stands for no context.

Here four configs, whose contexts are null, false, 5 and "ab", each take an ADD that assigns x, and a context function returns "ab"; describe prints a context, or the error of a snapshot that failed:

/** {x: 1}, XState's assign({ x: 1 }). */
xstate::result<boost::json::object> add_x(const xstate::action_args& /*args*/) {
    return boost::json::object{{"x", 1}};
}

/** A context function that returns a string, () => 'ab'. */
xstate::result<boost::json::value> make_ab(const boost::json::value& /*input*/) {
    return boost::json::value("ab");
}

/** A snapshot's context, or its error when it failed. */
std::string describe(const xstate::snapshot& now) {
    if (now.status == xstate::status::error) {
        return "error, " + now.error.message();
    }
    return boost::json::serialize(now.context);
}
xstate::implementations adder;
adder.actions.emplace("add", xstate::assign_action{.assignment = add_x});
const boost::json::value base = boost::json::parse(R"({
    "on": {"ADD": {"actions": "add"}}
})");
const boost::json::value contexts = boost::json::parse(R"([null, false, 5, "ab"])");
for (const boost::json::value& context : contexts.get_array()) {
    boost::json::object config = base.get_object();
    config["context"] = context;
    const xstate::result<xstate::machine> machine = xstate::create_machine(config, adder);
    if (!machine.has_value()) {
        return 1;
    }
    const xstate::snapshot initial = xstate::get_initial_snapshot(*machine);
    const xstate::snapshot added =
        xstate::get_next_snapshot(*machine, initial, {.type = "ADD", .payload = {}});
    std::cout << "context " << boost::json::serialize(context) << ": " << describe(initial)
              << ", after ADD " << describe(added) << '\n';
}

xstate::implementations maker;
maker.context = make_ab;
const xstate::result<xstate::machine> made =
    xstate::create_machine(boost::json::object{}, maker);
if (!made.has_value()) {
    return 1;
}
std::cout << "a context function that returns \"ab\": "
          << describe(xstate::get_initial_snapshot(*made)) << '\n';
The twin, on XState
import { assign, createMachine, getInitialSnapshot, getNextSnapshot, setup } from 'xstate';

function describe(snapshot) {
  if (snapshot.status === 'error') {
    return `error, ${snapshot.error}`;
  }
  return JSON.stringify(snapshot.context);
}

const adder = setup({ actions: { add: assign({ x: 1 }) } });
for (const context of [null, false, 5, 'ab']) {
  const machine = adder.createMachine({
    context,
    on: { ADD: { actions: 'add' } },
  });
  const initial = getInitialSnapshot(machine);
  const added = getNextSnapshot(machine, initial, { type: 'ADD' });
  console.log(`context ${JSON.stringify(context)}: ${describe(initial)}, after ADD ${describe(added)}`);
}

const made = createMachine({ context: () => 'ab' });
console.log(`a context function that returns "ab": ${describe(getInitialSnapshot(made))}`);
XState prints
context null: {}, after ADD {"x":1}
context false: {}, after ADD {"x":1}
context 5: 5, after ADD {"x":1}
context "ab": "ab", after ADD {"0":"a","1":"b","x":1}
a context function that returns "ab": {"0":"a","1":"b"}
xstate prints
context null: {}, after ADD {"x":1}
context false: false, after ADD error, implementation_failed
context 5: 5, after ADD error, implementation_failed
context "ab": "ab", after ADD error, implementation_failed
a context function that returns "ab": error, implementation_failed

Where imitating XState would make the API worse

XState reports a failure by throwing, accepts a config whose faults it finds only when an event reaches them, has its clock call the functions that fall due, and calls the listeners of an emitted event by a lookup of its type. Imitating the first two would let a program learn of a failure later than it can, or not at all; the clock would run the machine from inside the clock’s call, and the lookup would order the listeners by something other than the program. These entries say what xstate does instead.

Failures are values

XState

An implementation that fails throws. transition() and getNextSnapshot() let the error out; getInitialSnapshot() and an actor catch it into the snapshot, whose status is 'error', with the value and the context from before the event. A child that fails sends its parent xstate.error.actor.<id>, whose error is the value the implementation threw.

xstate

An implementation returns a result. A failure, implementation_failed or any other error_code the implementation returns, settles the macrostep with the snapshot it began from, its status error and its error that code, from the pure functions as from an actor, and the failing microstep returns no action. A cursor that drives microsteps one at a time also gets the actions the failing microstep resolved before it failed (When an implementation fails). A machine child that fails sends its parent xstate.error.actor.<id>, whose error is the child’s error_value when an error event no transition handled failed it, so a grandchild’s error climbs unchanged, as in XState, and otherwise the name of its error code, such as "implementation_failed" (Done and error events). The failed child’s snapshot keeps that code in error, and the error it reported in error_value, where XState’s keeps the value alone.

Why

xstate throws no exception, and builds where exceptions are off; a failure that comes back as a value is one the caller cannot miss: it is in the snapshot it receives, and that snapshot is the one an actor of XState keeps. An implementation fails with an error_code, which carries no value of its own, and an event’s members are JSON: the code’s name is the JSON that says which failure it was.

Here an assign fails on GO, first under the pure transition, then in an actor:

xstate::implementations implementations;
implementations.actions.emplace(
    "count",
    xstate::assign_action{
        .assignment = [](const xstate::action_args&) -> xstate::result<boost::json::object> {
            return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
        },
    });
const boost::json::value config = boost::json::parse(R"({
    "id": "m",
    "initial": "idle",
    "context": {"count": 0},
    "states": {
        "idle": {"on": {"GO": {"target": "busy", "actions": "count"}}},
        "busy": {}
    }
})");
const xstate::result<xstate::machine> machine = xstate::create_machine(config, implementations);
if (!machine.has_value()) {
    return 1;
}
const auto [next, actions] =
    xstate::transition(*machine, xstate::get_initial_snapshot(*machine),
                       xstate::event{.type = "GO", .payload = {}});
std::cout << "transition() returns: " << status_name(next.status) << ", "
          << boost::json::serialize(next.value) << ", " << next.error.message() << ", "
          << actions.size() << " actions\n";

xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> actor = system.create_actor(*machine);
if (!actor.has_value()) {
    return 1;
}
if (!system.start(*actor).has_value()) {
    return 1;
}
if (!system.send(*actor, xstate::event{.type = "GO", .payload = {}}).has_value()) {
    return 1;
}
const xstate::snapshot* failed = system.snapshot_of(*actor);
if (failed == nullptr) {
    return 1;
}
std::cout << "an actor's snapshot: " << status_name(failed->status) << ", "
          << boost::json::serialize(failed->value) << ", " << failed->error.message() << '\n';
The twin, on XState
import { assign, createActor, getInitialSnapshot, setup, transition } from 'xstate';

const machine = setup({
  actions: {
    count: assign(() => {
      throw new Error('count failed');
    }),
  },
}).createMachine({
  id: 'm',
  initial: 'idle',
  context: { count: 0 },
  states: {
    idle: { on: { GO: { target: 'busy', actions: 'count' } } },
    busy: {},
  },
});

try {
  transition(machine, getInitialSnapshot(machine), { type: 'GO' });
} catch (error) {
  console.log(`transition() throws: ${error.message}`);
}

const actor = createActor(machine);
actor.subscribe({ error: () => {} });
actor.start();
actor.send({ type: 'GO' });
const failed = actor.getSnapshot();
const value = JSON.stringify(failed.value);
console.log(`an actor's snapshot: ${failed.status}, ${value}, ${failed.error.message}`);
XState prints
transition() throws: count failed
an actor's snapshot: error, "idle", count failed
xstate prints
transition() returns: error, "idle", implementation_failed, 0 actions
an actor's snapshot: error, "idle", implementation_failed

Programs of the chapters meet the same difference too. In the example of guards, submit without feedback reaches a guard that reads it. XState’s guard throws a TypeError, which transition lets out behind Unable to evaluate guard; xstate’s returns implementation_failed, and the macrostep settles in the snapshot it began from, form, in error:

XState prints
rating 5 -> "thanks"
rating 1 -> "form"
rating 3 -> "neutral"
feedback "" -> "form"
feedback "Too slow." -> "submitting"
no feedback -> Unable to evaluate guard 'isValid' in transition for event 'submit' in state node 'feedback.form':
Cannot read properties of undefined (reading 'length')
xstate prints
rating 5 -> "thanks"
rating 1 -> "form"
rating 3 -> "neutral"
feedback "" -> "form"
feedback "Too slow." -> "submitting"
no feedback -> "form" error implementation_failed

In the example of a failing guard, the guard of an eventless transition fails after the microstep of GO. The C++ program drives the macrostep with the cursor, which reports that microstep, its actions and that it settled; XState has no cursor, and its twin’s actor prints only what it ran and how the macrostep ended, which the C++ program prints too:

XState prints
ran note
logged: say
status: error (implementation_failed)
value: "idle"
xstate prints
step: [note, xstate.emit, xstate.log], settled
ran note
logged: say
status: error (implementation_failed)
value: "idle"

In the example of error events, a payment fails under a checkout that has no onError, and the shop above it takes the checkout’s error. The error event is the same, and so is the error the checkout reported; xstate’s snapshot of the checkout also holds the code it failed with, which XState’s does not have:

XState prints
shop {"type":"xstate.error.actor.checkout","error":{"code":"declined"},"actorId":"checkout"}
checkout: error {"code":"declined"}
xstate prints
shop {"type":"xstate.error.actor.checkout","error":{"code":"declined"},"actorId":"checkout"}
checkout: error {"code":"declined"}
error code: actor_failed

A config is refused when it is created

XState

createMachine accepts configs it fails on later, or runs wrongly: an initial that names no child throws when the machine starts; a guard with no implementation throws when an event first evaluates it; a named after delay with no implementation counts as no delay, and the state is left at once; a state whose default entry leads back to it overflows the stack when it is entered; an entry, exit or actions that is null, which toArray reads as a list that holds null, throws a TypeError when that list runs, and a history’s target that is null, read the same way, when the default is needed; an invoke of an actor setup() does not hold warns, in the development build, and fails when something reads the child.

xstate

create_machine refuses each of these with an error and no machine, as it refuses every config XState refuses at creation (What creation refuses lists them all). It also refuses members that createMachine accepts, such as a history target that is not a string or a list of strings, the root’s too, and a tags that is null or holds anything but strings. One config that XState accepts, xstate accepts too: a history state at the root, whose default target has no parent to be resolved against, so a microstep that needs it fails with unknown_target where XState throws (History states).

Why

A machine is loaded from data when the program runs. The moment it is loaded is the moment its author can be told what is wrong with it, not when an event reaches the broken part, long after and perhaps in production.

Here report gives create_machine a config of each of these kinds but the null history target. The twin creates each with createMachine, then runs what reaches its fault and prints the first line of what that throws, or what it returns:

/** Prints whether create_machine accepts `config`, and the error when it refuses it. */
void report(std::string_view label, const boost::json::value& config) {
    const xstate::result<xstate::machine> created = xstate::create_machine(config, {});
    if (!created.has_value()) {
        std::cout << label << ": refused, " << created.error().message() << '\n';
        return;
    }
    std::cout << label << ": created\n";
}
const boost::json::value no_such_initial = boost::json::parse(R"({
    "id": "m",
    "initial": "nowhere",
    "states": {"a": {}}
})");
const boost::json::value unknown_guard = boost::json::parse(R"({
    "id": "m",
    "initial": "a",
    "states": {
        "a": {"on": {"GO": {"target": "b", "guard": "isReady"}}},
        "b": {}
    }
})");
const boost::json::value unknown_delay = boost::json::parse(R"({
    "id": "m",
    "initial": "a",
    "states": {
        "a": {"after": {"slow": "b"}},
        "b": {}
    }
})");
const boost::json::value endless_entry = boost::json::parse(R"({
    "id": "m",
    "initial": "a",
    "states": {
        "a": {
            "initial": "h",
            "states": {
                "h": {"type": "history", "target": "#m.a"},
                "x": {}
            }
        }
    }
})");
const boost::json::value null_exit = boost::json::parse(R"({
    "id": "m",
    "initial": "a",
    "states": {
        "a": {"exit": null, "on": {"GO": "b"}},
        "b": {}
    }
})");
const boost::json::value unknown_actor = boost::json::parse(R"({
    "id": "m",
    "invoke": {"id": "w", "src": "worker"}
})");
report("an initial that names no child", no_such_initial);
report("a guard with no implementation", unknown_guard);
report("a delay with no implementation", unknown_delay);
report("a default entry that leads back", endless_entry);
report("an exit that is null", null_exit);
report("an invoke of an unknown actor", unknown_actor);
The twin, on XState
import { createActor, createMachine, getInitialSnapshot, getNextSnapshot } from 'xstate';

// Prints that createMachine accepts `config`, then what `call` makes of the
// machine, or the first line of what it throws.
function report(label, config, name, call) {
  const machine = createMachine(config);
  console.log(`${label}: created`);
  try {
    console.log(`  ${name}: ${call(machine)}`);
  } catch (error) {
    console.log(`  ${name} throws: ${error.message.split('\n')[0]}`);
  }
}

const noSuchInitial = {
  id: 'm',
  initial: 'nowhere',
  states: { a: {} },
};
const unknownGuard = {
  id: 'm',
  initial: 'a',
  states: {
    a: { on: { GO: { target: 'b', guard: 'isReady' } } },
    b: {},
  },
};
const unknownDelay = {
  id: 'm',
  initial: 'a',
  states: {
    a: { after: { slow: 'b' } },
    b: {},
  },
};
const endlessEntry = {
  id: 'm',
  initial: 'a',
  states: {
    a: {
      initial: 'h',
      states: {
        h: { type: 'history', target: '#m.a' },
        x: {},
      },
    },
  },
};
const nullExit = {
  id: 'm',
  initial: 'a',
  states: {
    a: { exit: null, on: { GO: 'b' } },
    b: {},
  },
};
const unknownActor = {
  id: 'm',
  invoke: { id: 'w', src: 'worker' },
};

report('an initial that names no child', noSuchInitial, 'getInitialSnapshot', (machine) =>
  JSON.stringify(getInitialSnapshot(machine).value),
);
report('a guard with no implementation', unknownGuard, 'GO', (machine) =>
  JSON.stringify(getNextSnapshot(machine, getInitialSnapshot(machine), { type: 'GO' }).value),
);
report('a delay with no implementation', unknownDelay, 'the initial snapshot', (machine) =>
  JSON.stringify(getInitialSnapshot(machine).value),
);
report('a default entry that leads back', endlessEntry, 'the initial snapshot', (machine) => {
  const initial = getInitialSnapshot(machine);
  return `${initial.status}, ${initial.error}`;
});
report('an exit that is null', nullExit, 'GO', (machine) =>
  JSON.stringify(getNextSnapshot(machine, getInitialSnapshot(machine), { type: 'GO' }).value),
);
report('an invoke of an unknown actor', unknownActor, 'start()', (machine) => {
  const actor = createActor(machine);
  actor.subscribe({ error: () => {} });
  actor.start();
  return `${actor.getSnapshot().status}, ${actor.getSnapshot().error}`;
});
XState prints
an initial that names no child: created
  getInitialSnapshot throws: Initial state node "nowhere" not found on parent state node #m
a guard with no implementation: created
  GO throws: Unable to evaluate guard 'isReady' in transition for event 'GO' in state node 'm.a':
a delay with no implementation: created
  the initial snapshot: "b"
a default entry that leads back: created
  the initial snapshot: error, RangeError: Maximum call stack size exceeded
an exit that is null: created
  GO throws: Cannot read properties of null (reading 'type')
an invoke of an unknown actor: created
  start(): error, TypeError: Cannot read properties of undefined (reading 'getSnapshot')
xstate prints
an initial that names no child: refused, unknown_target
a guard with no implementation: refused, unknown_guard
a delay with no implementation: refused, unknown_delay
a default entry that leads back: refused, invalid_config
an exit that is null: refused, invalid_config
an invoke of an unknown actor: refused, unknown_actor

The last config also makes XState’s development build warn, on the standard error, that the actor type worker was not found.

Programs of the chapters meet the same difference too. In the example of a refused config, submit is guarded by isValid, which no implementation holds: XState creates the machine and throws when submit first evaluates the guard, and xstate refuses the machine:

XState prints
Unable to evaluate guard 'isValid' in transition for event 'submit' in state node '(machine).editing':
Guard 'isValid' is not implemented.'.
xstate prints
webcpp.xstate: unknown_guard

In the example of initial states, a parent state has no initial, and a machine’s initial names no child. XState and xstate refuse the first when the machine is created. XState creates the second and throws when the machine starts; xstate refuses it, with unknown_target:

XState prints
createMachine throws: No initial state specified for compound state node "#(machine).red". Try adding { initial: "walk" } to the state config.
created; getInitialSnapshot throws: Initial state node "nowhere" not found on parent state node #(machine)
xstate prints
invalid_config
unknown_target

The core’s clock hands over the due events

XState

SimulatedClock holds functions: setTimeout keeps one with its delay, and set and increment move the time and call each function that falls due, in the order of the deadlines. set throws for a time before the current one.

xstate

The core’s simulated_clock holds events. apply schedules each built-in delayed xstate.raise a step returns and applies each built-in xstate.cancel, a custom action being the caller’s whatever its type or its params, and schedule holds any event. set and increment only move the time, and pop_due() takes out the earliest event that is due, which the caller delivers as a macrostep of its own. set refuses a time before the current one with invalid_config, and the time stays where it was (Delayed events and the simulated clock).

Why

The machine core computes what a step does and leaves every effect to its caller, which delivers each event. A clock that called a function would run the machine from inside the clock’s call; one that hands over each due event lets the caller apply what its delivery returns before it takes the next, so a delivered event can cancel a later one, as it can in XState. A refusal is a value, as every failure is (Failures are values).

Listeners of emitted events

XState

actor.on(type, handler) adds a listener of one type, or of every type with *. An emitted event reaches the listeners of its type first and then those of *, each group in the order it was added.

xstate

on_emitted adds a listener that hears every event the actor emits, as XState’s actor.on('*'), and an actor’s listeners hear in the order they were added. A listener that stands for one of a type tests the type itself; adding those first keeps XState’s order.

Why

One kind of listener, heard in the order the program added it, is the whole rule. A type is one test in the listener, and the order follows from the program instead of from a lookup.

Here the listener of every type is added first, then a listener of PING, and GO emits PING:

xstate::implementations implementations;
implementations.actions.emplace(
    "ping", xstate::emit_action{
                .event = [](const xstate::action_args&) -> xstate::result<xstate::event> {
                    return xstate::event{.type = "PING", .payload = {}};
                },
            });
const boost::json::value config = boost::json::parse(R"({
    "on": {"GO": {"actions": "ping"}}
})");
const xstate::result<xstate::machine> emitter = xstate::create_machine(config, implementations);
if (!emitter.has_value()) {
    return 1;
}
xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> actor = system.create_actor(*emitter);
if (!actor.has_value()) {
    return 1;
}
const xstate::result<void> hearing_any =
    system.on_emitted(*actor, [](const xstate::event& heard) {
        std::cout << "the listener of every type heard " << heard.type << '\n';
    });
if (!hearing_any.has_value()) {
    return 1;
}
// XState's actor.on('PING', ...): a listener that tests the type itself.
const xstate::result<void> hearing_ping =
    system.on_emitted(*actor, [](const xstate::event& heard) {
        if (heard.type != "PING") {
            return;
        }
        std::cout << "the listener of PING heard " << heard.type << '\n';
    });
if (!hearing_ping.has_value()) {
    return 1;
}
if (!system.start(*actor).has_value()) {
    return 1;
}
if (!system.send(*actor, xstate::event{.type = "GO", .payload = {}}).has_value()) {
    return 1;
}
The twin, on XState
import { createActor, emit, setup } from 'xstate';

const emitter = setup({
  actions: {
    ping: emit({ type: 'PING' }),
  },
}).createMachine({
  on: { GO: { actions: 'ping' } },
});

const actor = createActor(emitter);
actor.on('*', (heard) => console.log(`the listener of every type heard ${heard.type}`));
actor.on('PING', (heard) => console.log(`the listener of PING heard ${heard.type}`));
actor.start();
actor.send({ type: 'GO' });
XState prints
the listener of PING heard PING
the listener of every type heard PING
xstate prints
the listener of every type heard PING
the listener of PING heard PING

The checks of XState’s development build

XState

Of XState’s two builds, the development build, which XState’s own tests run and node --conditions=development selects, makes checks that the production build leaves out, and throws where one fails. It also warns, on the standard error, about an event descriptor whose * is not its last token.

xstate

xstate makes the checks a JSON machine can meet, in every build, and writes no warning:

  • An event of the type * that a macrostep takes from outside fails the macrostep with invalid_event, the snapshot it began from in error. An event of that type that the machine raises itself takes the wildcard transitions, as in XState.

  • A transition that declares a cond, the name XState v4 gave guard, refuses the machine with invalid_config when the cond is truthy, as JavaScript reads it.

  • A forwardTo whose target resolves to no actor fails with unknown_target: one with an empty target, when it resolves, and, in an actor, one to #system:<systemId> when no actor holds the systemId.

Why

Each check stops an error that the production build lets through to do something worse. The production build ignores the cond, so the transition is taken unguarded; it runs an event of the type *, a type XState keeps for the wildcard descriptor; and it forwards an event whose target is undefined to the sender itself, which forwards it again, without end. xstate’s tests compare it with the development build (XState as the oracle), and xstate behaves as that build does. A warning changes nothing that a machine does, and xstate writes to no stream: what it has to tell its caller comes back as a value.

The program makes the three checks, one config each:

const boost::json::value with_cond = boost::json::parse(R"({
    "id": "m",
    "initial": "a",
    "states": {
        "a": {"on": {"GO": {"target": "b", "cond": "ready"}}},
        "b": {}
    }
})");
const xstate::result<xstate::machine> refused = xstate::create_machine(with_cond, {});
std::cout << "a transition that declares cond: "
          << (refused.has_value() ? "created" : refused.error().message()) << '\n';
const boost::json::value listener = boost::json::parse(R"({
    "id": "m",
    "on": {"*": {"actions": "heard"}}
})");
const xstate::result<xstate::machine> anything = xstate::create_machine(listener, {});
if (!anything.has_value()) {
    return 1;
}
const xstate::snapshot after_star =
    xstate::get_next_snapshot(*anything, xstate::get_initial_snapshot(*anything),
                              xstate::event{.type = "*", .payload = {}});
std::cout << "an event of the type * from outside: " << status_name(after_star.status) << ", "
          << after_star.error.message() << '\n';
xstate::implementations implementations;
implementations.actions.emplace("relay", xstate::forward_to_action{
                                             .target = "#system:nobody",
                                             .id = std::nullopt,
                                             .delay = std::nullopt,
                                         });
const boost::json::value relay_config = boost::json::parse(R"({
    "id": "relay",
    "on": {"PING": {"actions": "relay"}}
})");
const xstate::result<xstate::machine> relay =
    xstate::create_machine(relay_config, implementations);
if (!relay.has_value()) {
    return 1;
}
xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> actor = system.create_actor(*relay);
if (!actor.has_value()) {
    return 1;
}
if (!system.start(*actor).has_value()) {
    return 1;
}
if (!system.send(*actor, xstate::event{.type = "PING", .payload = {}}).has_value()) {
    return 1;
}
const xstate::snapshot* forwarded = system.snapshot_of(*actor);
if (forwarded == nullptr) {
    return 1;
}
std::cout << "a forwardTo to no actor: " << status_name(forwarded->status) << ", "
          << forwarded->error.message() << '\n';

Then it sends four events to a state whose two descriptors each hold a * before their last token, mouse.*.* and mou*se.*. The twin prints each warning XState’s development build writes, under the event that made it:

const boost::json::value pointer_config = boost::json::parse(R"({
    "id": "pointer",
    "initial": "idle",
    "states": {
        "idle": {"on": {"mouse.*.*": "deep", "mou*se.*": "starred"}},
        "deep": {},
        "starred": {}
    }
})");
const xstate::result<xstate::machine> pointer = xstate::create_machine(pointer_config, {});
if (!pointer.has_value()) {
    return 1;
}
const xstate::snapshot idle = xstate::get_initial_snapshot(*pointer);
for (const std::string_view type : {"mouse.x", "mouse.x", "mou*se.x", "mouse.*.*"}) {
    const xstate::snapshot next = xstate::get_next_snapshot(
        *pointer, idle, xstate::event{.type = std::string(type), .payload = {}});
    std::cout << "the event " << type << ": " << boost::json::serialize(next.value) << '\n';
}
The twin, on XState
import { createActor, createMachine, forwardTo, getInitialSnapshot, getNextSnapshot } from 'xstate';

try {
  createMachine({
    id: 'm',
    initial: 'a',
    states: {
      a: { on: { GO: { target: 'b', cond: 'ready' } } },
      b: {},
    },
  });
  console.log('a transition that declares cond: created');
} catch (error) {
  console.log(`a transition that declares cond: throws: ${error.message}`);
}

const anything = createMachine({
  id: 'm',
  on: { '*': { actions: 'heard' } },
});
try {
  const start = getInitialSnapshot(anything);
  const afterStar = getNextSnapshot(anything, start, { type: '*' });
  console.log(`an event of the type * from outside: ${afterStar.status}`);
} catch (error) {
  console.log(`an event of the type * from outside: throws: ${error.message}`);
}

const relay = createMachine({
  id: 'relay',
  on: {
    PING: { actions: forwardTo(({ system }) => system.get('nobody')) },
  },
});
const actor = createActor(relay);
actor.subscribe({ error: () => {} });
actor.start();
actor.send({ type: 'PING' });
const forwarded = actor.getSnapshot();
console.log(`a forwardTo to no actor: ${forwarded.status}, ${forwarded.error?.message}`);

// The development build warns about these descriptors on the standard
// error; each warning is printed here, under the event that made it.
const warnings = [];
console.warn = (message) => warnings.push(message);
const pointer = createMachine({
  id: 'pointer',
  initial: 'idle',
  states: {
    idle: { on: { 'mouse.*.*': 'deep', 'mou*se.*': 'starred' } },
    deep: {},
    starred: {},
  },
});
const idle = getInitialSnapshot(pointer);
for (const type of ['mouse.x', 'mouse.x', 'mou*se.x', 'mouse.*.*']) {
  const next = getNextSnapshot(pointer, idle, { type });
  console.log(`the event ${type}: ${JSON.stringify(next.value)}`);
  for (const warning of warnings.splice(0)) {
    console.log(`  warns: ${warning}`);
  }
}
XState prints
a transition that declares cond: throws: State "m.a" has declared `cond` for one of its transitions. This property has been renamed to `guard`. Please update your code.
an event of the type * from outside: throws: An event cannot have the wildcard type ('*')
a forwardTo to no actor: error, Attempted to forward event to undefined actor. This risks an infinite loop in the sender.
the event mouse.x: "idle"
  warns: Wildcards can only be the last token of an event descriptor (e.g., "event.*") or the entire event descriptor ("*"). Check the "mouse.*.*" event.
  warns: Infix wildcards in transition events are not allowed. Check the "mouse.*.*" transition.
  warns: Wildcards can only be the last token of an event descriptor (e.g., "event.*") or the entire event descriptor ("*"). Check the "mou*se.*" event.
the event mouse.x: "idle"
the event mou*se.x: "starred"
  warns: Wildcards can only be the last token of an event descriptor (e.g., "event.*") or the entire event descriptor ("*"). Check the "mouse.*.*" event.
  warns: Wildcards can only be the last token of an event descriptor (e.g., "event.*") or the entire event descriptor ("*"). Check the "mou*se.*" event.
the event mouse.*.*: "deep"
  warns: Wildcards can only be the last token of an event descriptor (e.g., "event.*") or the entire event descriptor ("*"). Check the "mou*se.*" event.
xstate prints
a transition that declares cond: invalid_config
an event of the type * from outside: error, invalid_event
a forwardTo to no actor: error, unknown_target
the event mouse.x: "idle"
the event mouse.x: "idle"
the event mou*se.x: "starred"
the event mouse.*.*: "deep"

The first three lines of XState’s are the messages its checks throw. Its production build makes none of them: it creates the machine that declares cond, runs the event of the type * and ends its macrostep active, and never returns from the send() that forwards to no actor.

The warnings come from the matching of a descriptor that ends in .* and holds another *. XState warns that a wildcard may only be the last token of a descriptor, and, when a token that is * alone comes before the last and the event’s tokens before it equal the descriptor’s, as the mouse of mouse.x equals that of mouse.*.*, also that infix wildcards are not allowed. A state warns about each such descriptor of its own but the one equal to the event’s type, the first time it looks up the transitions of a type. It keeps what it found for each type, so the same type sent again writes nothing unless the search reaches a state that has not looked that type up yet, such as a parent the first send did not reach. Every descriptor matches as in the production build, and as in xstate (Event descriptors): mouse.*.* only the event of that exact type, and mou*se.*, a partial descriptor whose first token is mou*se, mou*se.x.

The example of event descriptors ends with an event of the type * from outside, which XState’s development build throws on and xstate fails with invalid_event:

XState prints
feedback.good -> "thanks"
feedback.bad -> "apology"
feedback.bad.rude -> "apology"
feedback.meh -> "form"
feedback -> "form"
feedbacks -> "other"
mouse.click -> "other"
* -> An event cannot have the wildcard type ('*')
xstate prints
feedback.good -> "thanks"
feedback.bad -> "apology"
feedback.bad.rude -> "apology"
feedback.meh -> "form"
feedback -> "form"
feedbacks -> "other"
mouse.click -> "other"
* -> invalid_event

Appendix C: Rationale

This appendix gives the decisions that shape xstate, each with its reason, the alternative it was weighed against where there was one, and what it costs. The chapters teach what each decision makes of the library, so a section here says why and points to the chapter rather than teach it again. Where a decision makes xstate behave otherwise than XState, Appendix B gives the case (Where xstate differs from XState).

Why it is built this way

Three aims shape xstate, and each decision below serves at least one of them:

XState’s machines, unchanged

A config written for XState, or exported from Stately Studio, runs in a C++ program and does there what it does in XState, transition for transition. A team keeps one machine for both languages, and XState itself can judge the port.

A host in control

The program that runs a machine may not have written it, and stays in charge of it all the same: of the work an event may cost, of time, of what a failure becomes and of every effect on the world, with the same run for the same inputs.

A library in Boost’s manner

Portable to every C++20 compiler and to WebAssembly, usable in every build a program makes, one without exceptions included, and built, tested and documented as Boost’s libraries are.

The first aim gives the JSON config, the implementations registered by name, the actor layer that runs machines as XState’s actors, and the oracle. The second gives errors as values, the cursor and fuel, and determinism. The third gives header-only, no exception of its own, and Boost’s tools. Where the first two pull apart, the host’s control wins: XState runs a macrostep without a bound, and delivers an event nested in its sender’s call, where xstate bounds every macrostep with fuel and queues every delivery. Appendix B lists each such place, with its reason (Where xstate differs from XState).

A JSON config

A machine is the JSON of XState’s createMachine config, a boost::json::value read when the program runs (The config).

  • A machine is data. It is loaded from a file, a request or a store, with nothing to compile, so a program runs machines that did not exist when it was built: one a user wrote, one Stately Studio exported, one a request carried.

  • It is XState’s format. The config is the one a person writes for XState’s createMachine, and to_json writes a machine’s definition as XState’s machine.toJSON() writes it (The definition). The same config runs on XState and on xstate, and that is what lets XState judge the port: every case of the oracle is a config both run (The oracle and the twins as evidence).

  • Boost.JSON holds it. It is Boost’s JSON library, and its object keeps its members in the order they were written, which xstate needs to read states, on and after in the order XState reads them.

The alternative is a machine described in C++, by a builder or by a table of types. The compiler checks it, but it is code: it cannot come from a file or from Stately Studio, and XState cannot run it. Such a machine is what compile-time libraries such as Boost.MSM already provide (Static actors left to compile-time machines).

The cost is what JSON cannot hold. A function, undefined and a reference to an actor each take another form (What JSON imposes, and From XState to C++ for the spelling). And a mistake in a config is found when the program loads it, not when the program is compiled: so create_machine checks all it can at that moment, and refuses some configs that XState accepts and fails on only later (A config is refused when it is created).

Implementations registered by name

What JSON cannot hold, the code of an action, a guard, a delay or an actor, and the functions that compute a context, an input or an output, is C++ registered in implementations under the name the config gives it (Implementations).

  • XState already does it. setup() registers implementations by name, and the config refers to them by name, with JSON params. A machine written that way keeps its config unchanged in xstate (Functions become implementations registered by name).

  • Names are checked once. A guard, an after delay or an actor that the config names and the implementations do not hold refuses the machine when it is created, before any event reaches the part that would fail (A config is refused when it is created).

  • Effects stay the program’s. An action name the implementations do not hold is a custom action, which the machine returns with its params, for its caller to perform (Custom actions). The machine says what is to be done, and the program decides how.

An implementation receives the context, the event and the params, its action_args, and returns a result. It receives no actor and no system, where XState’s does: an effect that needs them is a custom action, which the host performs. The cost falls there: a custom action carries only its fixed params, so what it needs of the context or the event is in them, or travels in an emitted event (Custom actions).

Header-only

xstate is headers, as xactor is, and nothing of either is built, versioned or linked. The first chapter gives the first reason, portability (Why header-only): the same headers build the same way with every C++20 compiler, natively and for WebAssembly, with no binary to match to each. Two more follow:

  • The program’s flags are the library’s. The headers are compiled with the program, so a build without exceptions, or with a sanitizer, gets the library built the same way, which a prebuilt library cannot promise.

  • xactor is generic. Its scheduler, its turns, its actor logic and its drivers are templates over the program’s message type, and a template is a header in any case (xactor’s model).

The one part a program compiles is Boost.JSON’s definitions: one of its translation units includes <boost/json/src.hpp>, which is how Boost.JSON itself is used without a compiled library (Requirements). Boost.System and Boost.Asio are headers too, so no Boost library is built, neither by a program nor by webcpp, whose Boost.Test suites use its header-only form.

The cost is compile time: each translation unit compiles what it includes. A program that only steps machines includes <webcpp/xstate.hpp> alone, which brings in nothing of xactor or of Boost.Asio (Two layers).

Errors returned, not thrown

Every call that can fail returns a result<T>, Boost.System’s result with an error_code; an implementation fails by returning one; and a machine that fails while it runs settles in a snapshot whose status is error (Errors as values).

  • A failure cannot pass unseen. A caller reaches a value only through its result, and the failure of a run is in the snapshot the caller receives: the snapshot XState’s actor keeps when an implementation throws, which xstate’s pure functions return too.

  • Every build can use it. A value works where exceptions are off (No exception of its own), and an exception does not.

  • A code travels. An error_code carries its category, compares with the codes of xstate::errc and xactor::errc, and holds a code of any other library as well: an implementation may return the code its own call failed with, and the snapshot keeps it as it is.

The alternatives fall short. std::expected is C++23, and xstate is C++20. Boost.System’s result is the type Boost.JSON’s own checking accessors, such as try_at, return. And a failure carried by an exception would not reach a caller in the builds xstate is made for.

The cost: an error_code carries no value, where XState’s implementation may throw any value. A machine child that fails in an implementation tells its parent the name of its code, "implementation_failed", where XState’s tells what was thrown (Failures are values).

No exception of its own

xstate throws no exception and catches none (guarantee 15), and neither does xactor (xactor’s guarantee 20).

  • Programs turn them off. Many builds disable exceptions: embedded and real-time programs, games, and WebAssembly for a host that lacks exception handling. A header-only library is compiled with the program’s flags (Header-only), so it must work under the strictest of them; one that needed exceptions would make every program that uses it turn them on. Whether a program uses them stays its own choice.

  • No hidden exit. A call that throws nothing returns through its result, so every way out of it is one its caller sees (Errors returned, not thrown).

The lint keeps it so: it compiles every public header, and every test and example, which instantiate its templates, without exceptions (Targets), while every program, built for wasm32-wasip2 as for every target, is built with them. Where Boost’s own code would throw, a build without exceptions calls boost::throw_exception, which the program defines. In such a build that call is a defect, never a condition, and webcpp’s definition reports it and aborts (Requirements).

The macrostep cursor and fuel

The machine core’s one primitive is the macrostep cursor, whose next() runs one microstep; XState’s pure functions are the cursor run to its end (The macrostep cursor). The actor layer drives the cursor under fuel. Every microstep, every message an actor sends and every timer it arms costs one unit, paid from one reservoir for each execution, and an actor that runs out parks until resume() (Fuel, parking and resume, and xactor’s fuel).

  • A host must bound the work. XState runs a macrostep to its end inside one call. A program that runs machines it did not write cannot know that each one settles: a cycle of eventless transitions never does, and a server answers within a budget whatever machine it runs.

  • The bound is the host’s. XState’s options.maxIterations is a member of the config, set by the machine’s author and unset by default, and it throws when it is reached (A macrostep that never settles). xstate’s bound is set by the program that runs the machine, and stopping keeps the work: the cursor is a value, which resumes exactly where it stopped.

  • The unit is XState’s step. A microstep is the smallest step of XState’s algorithm, so a run stops only between two of them, never inside one.

  • One reservoir for each execution. Fuel counted for each message would bound the length of a chain of messages but not a fan-out, whose every branch would start with almost the whole budget. One reservoir, from which every actor of an execution pays, bounds its total work whatever its shape (xactor’s fuel).

The cost: a host chooses a budget, and an execution that spends it ends with out_of_fuel and a parked actor, which holds the host’s later deliveries until resume() (Appendix B, Fuel). XState has no counterpart, so the examples of the cursor and of fuel have no twin.

Determinism

The same inputs give the same run. xactor runs actors on one thread, one message per turn, from a first-in first-out mailbox for each actor and one first-in first-out queue of the actors that have work. Neither library reads a clock, a file or a network: time is a number the host brings with clock_tick, or, in the machine core, a simulated_clock its caller moves. And neither holds static mutable state (xactor’s rules of determinism, How the layer runs).

  • A run can be reproduced. A test replays a run by repeating its inputs, and a failure seen once is seen again. The oracle compares each actor’s events and snapshots with XState’s (XState as the oracle), and two schedulers fed the same deliveries record equal envelope logs, under either driver (xactor’s envelope log).

  • Time is an input. A delayed transition is tested without waiting, and the program decides what time is: the wall clock, a simulated one, or the time of a run it replays (Delayed events and time).

  • One thread needs no lock. The actors share nothing another thread could change, so they need no synchronisation, and they run the same everywhere: every example prints on wasm32-wasip2 and wasm32-wasip3 the output it prints natively (The examples).

The cost: XState delivers an event to an idle actor at once, nested in its sender’s call, and xactor queues it, so the order of events across actors can differ from XState’s, though never within one actor (Interleaving across actors). And the host owns the clock: a timer fires only when the host calls clock_tick.

The actor layer on xactor

xstate runs machines as XState’s actors on xactor, webcpp’s actor system, rather than on a second actor system of its own. Each actor that runs a machine is an xactor actor whose logic drives the machine’s cursor under its turn’s fuel. The layer adds what xactor leaves out: a child’s done and error events reaching its parent, invoke, the systemId registry, emitted events and inspection (The actor layer, What is not ported).

  • XState separates the two. A machine is logic, and an actor system runs it. xactor already is such a system, with a mailbox for each actor, a scheduler, timers, children that stop with their parent and the fuel of an execution, under XState’s names: actor_logic, create_actor, spawn_child, actor_ref. A second system inside xstate would duplicate all of it.

  • Machines inherit xactor’s guarantees. Determinism and the bound on work hold for a machine’s actor as for any actor of xactor (Determinism, The macrostep cursor and fuel).

  • Each library stands alone. xactor knows nothing of xstate, and a program uses it on its own for actors written in C++ (xactor’s page). The machine core knows nothing of xactor: <webcpp/xstate.hpp> includes none of it, which a check of the build keeps so, and a program that only steps machines does not depend on xactor.

The cost: XState’s actors are reached through xactor’s model, so the deliveries are queued and the work is paid for, which Appendix B lists entry by entry (What the actor model on xactor imposes).

Static actors left to compile-time machines

xstate interprets machines that are data; it does not compile a machine into C++ types. A program whose machine is known when it is written can write it with a compile-time state machine library, such as Boost.MSM, and run it as an xactor actor whose logic holds the machine (xactor’s static actors).

  • Those libraries already do it well. A compile-time machine is what they are for: the compiler checks the transition table, and nothing is read or interpreted when the program runs. A third such library inside xstate would duplicate them.

  • xactor needs no adapter. An actor logic is any class with a handle, so a logic holds a machine as a member and hands each message to it. xactor depends on no such library, and any library whose machines take typed events joins the same way.

The cost: a static machine cannot come from JSON or from Stately Studio, and XState cannot run it, so nothing compares it with XState.

Boost’s way of building and testing

xstate is built, tested and documented as Boost’s libraries are, as every library of webcpp is, so that people who know these tools can review it:

  • b2 is the build, of everything: the tests, the examples and this page, from the root of a webcpp checkout, and a b2 project uses xstate through /webcpp/xstate//xstate (Building with b2).

  • Boost.Core’s lightweight_test runs the tests, a header of its own, so that a test runs as it is on every target, and in a user’s build without exceptions; Boost.Test runs the 2 suites that register a test case for each ported case, natively, in its header-only form, so the build compiles no Boost library (The tests).

  • Every target is tested. The tests and the examples run natively and for wasm32-wasip2 and wasm32-wasip3, each with exceptions, and the lint compiles every public header, and every test and example, without them, so that a library that works without them is a checked fact rather than a claim. Every public header also compiles on its own, and the machine core is checked to include nothing of xactor (How xstate is tested).

  • Every program of this page is a test. b2 libs/xstate/example builds each one, runs it and compares what it printed with the output committed beside it, on every target, and the page includes that source and that output, so neither can drift from the library (The examples).

  • The documentation is one page of AsciiDoc, built with Asciidoctor.js on Node, which the oracle needs already, so the build needs neither Ruby nor Java; its reference is written by MrDocs from the Doc Comments of the headers, and a message of Asciidoctor’s fails the build.

The continuous integration runs all of it with GCC, Clang, Apple Clang and MSVC, and for wasm32-wasip2 and wasm32-wasip3 (Targets).

The oracle and the twins as evidence

XState itself proves xstate. Every case ported from XState’s tests, and every case written where they are silent, runs on XState 5.33.2 and on xstate, and xstate’s result must equal XState’s, microstep by microstep and actor by actor; and every program of this page that uses xstate runs on XState too, as its twin (How the port is proven, How xstate is tested).

  • A port is right when it does what the original does. An expectation written by hand states what its writer believed XState does, and a mistake in it would hold xstate to the mistake. What XState produces is what XState does.

  • The development build is the judge, as it is of XState’s own tests; where its checks can meet a JSON machine, xstate makes them too (The checks of XState’s development build).

  • What XState produced is committed, so the suites run without Node, and the oracle lane runs XState again and fails when what it produces is no longer what the suites hold (Regenerating XState’s output).

  • The twins hold this page to XState. A program shown here that XState can run has run on XState and printed what the page shows. Where xstate differs on purpose, the twin’s output is XState’s own, and Appendix B shows the two (Where xstate differs from XState).

The cost is Node, for the oracle and for this page; and the programs that show what XState does not have, the cursor and fuel among them, have no twin, so only their own committed output proves them (The XState twins).

Appendix D: Glossary

An alphabetical guide to the terms this documentation uses. Each entry says what the term means here, what XState calls it, what it is in the code of xstate, and which section explains it.

Most terms are XState’s and keep XState’s meaning; xstate adds C++ names for them. A few are xstate’s own, for things XState does not have: the cursor, settled, fuel, an execution, a parked actor and xactor; and a host actor, with its host request, stands where XState runs a promise. xactor’s own terms name the parts of the actor system the actor layer runs on, and its page defines them: the scheduler, a turn, an actor logic, a mailbox, an envelope and the envelope log, a driver and a static actor. The last subsections show the terms at work in programs: a machine, an actor system, and an actor system that runs out of fuel.

Terms

Action

An effect a machine names on a state’s entry or exit, or on a transition, and runs when it passes there. Actions are fire-and-forget: the machine names them and moves on. A few are built in, and the library knows what each means (built-in action); every other one is a custom action, which the machine hands to its caller. A microstep returns its actions in XState’s order: the exit actions of the states it leaves, the actions of its transitions, then the entry actions of the states it enters.

XState: a name, an action object { type, params } or an inline function; transition() returns them as action objects.
xstate: a name or {"type": ..., "params": ...} in the config, an implementation in implementations.actions, and an xstate::action (type, params) when it is returned. An inline function cannot be JSON, so it becomes a named action.
See Entry, exit and transition actions.

Actor

A running process that receives events, sends events to other actors, and changes its behaviour with what it receives. Nothing else reaches its state: other actors only send it messages. In xstate an actor runs a machine (a machine actor) or stands for work the host does (a host actor), inside an actor system. The host creates a root actor and starts it, which runs its initial macrostep; a machine creates its children by invoking or spawning them.

XState: an actor, from createActor(logic), named by an ActorRef; what it runs is its actor logic.
xstate: an actor of an xstate::actor_system, named by an xstate::actor_ref; create_actor and start for a root; machine_actor and host_actor for what a child runs, by its src.
See Actors in Core concepts and Creating an actor.

Actor layer

The half of xstate that runs machines as XState’s actors: the host creates an actor from a machine, starts it, sends it events and reads its snapshot, and the layer executes what the machine core returns. It creates the child of a spawn, delivers the event of a send, arms a timer for a delayed raise or send, and hands a custom action to the host. It runs on xactor: each actor that runs a machine is an xactor actor whose logic steps the machine.

XState: createActor and the system it creates, which XState’s package holds beside its machines.
xstate: <webcpp/xstate/actors.hpp>, whose xstate::actor_system holds the actors.
See The actor layer and Actors: xstate on xactor.

Actor system

The actors that run together and what they share: the registry of systemIds, the scheduler that delivers their messages, and the host’s listeners. The host creates actors in it, and creating one runs nothing. Starting an actor, sending it an event, moving the time, answering a request and resuming each run the system until no message is left, and return out_of_fuel when an actor is left parked; while one is parked, every one of these calls but resuming is held instead (parked actor).

XState: the system each root actor creates, actor.system, with system.get(systemId).
xstate: xstate::actor_system, built with the fuel of one execution (xstate::budgets); the roots created in one actor_system share one system, where XState makes one for each root (One system per actor_system).
See The system.

After transition

See delayed transition.

Always transition

See eventless transition.

Assign

The built-in action that changes the context. Its implementation returns a partial object, which is merged into the context as Object.assign merges it. Once a machine has started, it is the only way its context changes. An assign is never returned to the caller: its effect is already in the snapshot.

XState: assign({ ... }), which spreads whatever the context is, so a context "ab" becomes {"0": "a", "1": "b"} beside the update.
xstate: xstate::assign_action, whose assignment, an xstate::assigner, returns the partial update as a result<boost::json::object>; an assign on a context that is not an object fails with implementation_failed (A context that is not an object).
See Assign and Updating context with assign.

Atomic state

A state without child states, a leaf of the machine’s tree, such as green in a traffic light. Each active branch of a machine ends in an atomic state or a final one.

XState: type: 'atomic', which a state without states has.
xstate: the same; node_type::atomic.
See Parent states in Core concepts and Selecting a transition.

Built-in action

An action whose meaning the library knows: assign, raise, log, sendTo, sendParent, forwardTo, cancel, spawnChild, stopChild and emit. The library resolves its params when its microstep runs; every one but assign is then returned to the caller, or to the actor layer, which carries it out, under a type that starts with xstate. (the names that start with xstate.).

XState: the action creators assign, raise, sendTo and the rest.
xstate: the alternatives of xstate::action_implementation, registered by name in implementations.actions; a returned one has action::builtin set.
See Built-in and custom actions.

Child actor

An actor another actor, its parent, started by invoking it in a state or spawning it with an action. The parent knows it by its id, may send it events, and hears when it is done or fails. A child stops when its parent stops it, when the state that invoked it is left, or when its parent is done or stopped.

XState: snapshot.children, by id.
xstate: snapshot::children, a list of (id, src); actor_system::child_of(parent, id) and parent_of(child).
See Invoking actors, Spawning actors and Stopping a child.

Clock

What holds delayed events until their time comes. A machine never reads a clock: the time is its caller’s. With the machine core, a simulated_clock the caller owns holds the built-in delayed raises a step returned and releases each once the time the caller sets reaches it; a custom action is the caller’s, whatever its type or params. In the actor layer, each delayed event is a timer of the actor that armed it, and the host moves the time with clock_tick(now). Time counts whole milliseconds.

XState: the clock option of createActor, setTimeout by default, and SimulatedClock for tests.
xstate: xstate::simulated_clock (apply, schedule, cancel, increment, set, pop_due); actor_system::clock_tick(now).
See Delayed events and the simulated clock and Delayed events and time.

Compound state

See parent and child states.

Config

The JSON that describes a machine: its states, their transitions, and the names of the actions, guards, delays and actors it uses. It is the object a person writes for XState’s createMachine, or exports from Stately Studio. A config holds no function: where XState takes a function, xstate takes a name, and the function is registered under that name in the implementations.

XState: the object given to createMachine(config).
xstate: a boost::json::value, read by create_machine(config, implementations), which refuses every config XState refuses, some earlier than XState does, with an error code and no machine.
See The config.

Context

The data a machine keeps beside its finite state: a count, the fields of a form, what a child returned. It is a JSON value: the config’s context, or {} when the config gives none or null, or what a context function makes of the input. It then changes only through assign.

XState: snapshot.context. A falsy context in the config, such as false, becomes {}, and what a context function of the input returns is merged into {}.
xstate: snapshot::context, a boost::json::value. A config’s context that is not an object, null aside, is kept as written, and an assign on it fails with implementation_failed. The context function is implementations.context, a context_maker, which returns a result<boost::json::value>; a value that is not an object fails the initial macrostep with implementation_failed (A context that is not an object).
See Context.

Core

See machine core.

Cursor

xstate’s one primitive: a value that runs a macrostep one microstep at a time. Each call of next() runs exactly one microstep and says whether it settled the macrostep. The caller decides how many microsteps to pay for, keeps the cursor between calls, and may copy it: a copy resumes exactly where the original stopped. Each pure transition function is the cursor run to its end.

XState: none. XState runs a macrostep to its end inside transition() or actor.send().
xstate: xstate::macrostep, from begin(machine, snapshot, event) or begin_initial(machine, input); next() returns a progress (step, settled).
See The macrostep cursor.

Custom action

An action the implementations do not hold. The machine does nothing with it: it returns it to the caller, with its name and the params the config gave, in the order XState would run it, and the caller runs it. This is how a program’s own effects, sending a mail or writing a row, leave a machine.

XState: an action whose implementation is a function of the program, which a running actor calls and transition() returns as an action object.
xstate: an xstate::action whose builtin is false; in the actor layer, handed to the on_action listener.
See Custom actions.

Deferred effect

What an actor of the actor layer does only once its macrostep has settled: the delivery or the timer of a send, the timer of a delayed raise, a cancel, an emit, the stop of a started child and the start of a new one. The actor first publishes its snapshot, then runs them in the order its actions deferred them; on its start, the starts of its active children come first, as XState’s start() starts them before the actor’s own initial actions.

XState: the work its actions put off with actorScope.defer, which an actor runs once its snapshot is updated.
xstate: inside the actor layer; the host sees their outcome.
See Sending events between actors and Watching an actor.

Definition

A machine written back as JSON, in the form XState’s machine.toJSON() writes. It lists every state, transition and action by name, but leaves out the context, the eventless transitions and a history state’s default target, and two states may share an id in it when a key holds a dot. A machine loaded back from it, given its context, takes the original’s steps and writes the same definition, its version included, whenever the original has none of those and no entry or exit names a custom action xstate.raise or xstate.cancel: a definition cannot tell such an action from the raise and cancel an after adds, and the loader drops it.

XState: machine.toJSON(), which leaves out a higher-order guard.
xstate: to_json(machine), which writes the higher-order guards too, and create_machine_from_definition(definition, implementations, context), which refuses with invalid_config a member of another type than XState writes, such as an entry that is not a list.
See The definition.

Delayed event

An event raised or sent with a delay, delivered when its time comes rather than at once. A delayed raise comes back to the machine itself; a delayed send goes to another actor. Either may carry an id, which a cancel names to drop it before it fires. The delay is a number of milliseconds or the name of a delay implementation.

XState: raise and sendTo with { delay, id }, cancel(id), and setup({ delays }).
xstate: the delay and id of raise_action and of the send actions, cancel_action, and implementations.delays. The machine returns the action. With the machine core, a simulated_clock holds a built-in delayed raise, while a delayed send is the caller’s to deliver: the clock keeps only its id, when it has one, as XState’s scheduler keys every delayed event by its id. In the actor layer, a timer of the sender holds either.
See Delayed raise, ids and cancel.

Delayed transition

A transition taken when its state has been active for a given time, unless the state is left first. A state’s after adds, on entry, a delayed raise of the event xstate.after.<delay>.<state id> and, on exit, the cancel of it, so the timer of a state already left never fires.

XState: after: { 1000: 'next' }.
xstate: the same after in the config; entering the state returns an xstate.raise with a delay, and leaving it an xstate.cancel.
See Delayed transitions.

Description

See meta.

Development build

The build of XState that makes checks its production build leaves out, and throws where one fails; it also warns, on the standard error, about an event descriptor whose * is not its last token. XState’s own tests run on it, and so does the oracle xstate’s tests compare with. xstate makes the checks a JSON machine can meet, in every build, and writes no warning.

XState: the build node --conditions=development selects.
xstate: invalid_event for an event of the type * a macrostep takes from outside, invalid_config for a transition with a truthy cond, and unknown_target for a forwardTo whose target resolves to no actor.
See The checks of XState’s development build.

Document order

The order of a depth-first walk of a machine’s tree: the root first, and each node followed by all its descendants before its next sibling. Siblings come in the order JavaScript enumerates the keys of their parent’s states: a key that is an array index, such as "2" or "10", first, by number, then the others as the config writes them. Unless two nodes share an id, a microstep exits states in reverse document order and enters them in document order, the regions of a parallel state included: both libraries sort them by XState’s order, which falls behind a node’s place in document order after a node that takes an id an earlier node holds. JavaScript enumerates the keys of a state’s on and after the same way as those of states, and XState and xstate read them so: a state whose after writes 1000 before 500 returns on entry the raise of 500 before that of 1000.

XState: StateNode.order, from states read with Object.keys.
xstate: the index of a node, as in machine::node(index); state_node::order, XState’s order, the number of distinct ids among the nodes before it.
See State nodes and ids and Nested states and the root.

Done event

The event that says something finished. When a compound state’s final child is entered, the machine raises xstate.done.state.<state id> for the compound state, with that child’s output; when every region of a parallel state is final, it raises the parallel state’s, with no output. The state’s onDone takes it. When a child actor is done, its parent receives xstate.done.actor.<child id>, with the child’s output and its actorId, and the invoke’s onDone takes it.

XState: the same two events.
xstate: the same; an onDone is a transition on the event’s type.
See A parent state is done, Done events and Done and error events.

Emitted event

An event an actor emits to the listeners outside it, not to another actor. Emitting changes nothing in the machine.

XState: emit(event), heard with actor.on(type, handler).
xstate: emit_action, returned as xstate.emit; in the actor layer, heard with actor_system::on_emitted(actor, listener), which hears every type, as XState’s actor.on('*', ...) does.
See Emitting events and Watching an actor.

Error code

What xstate returns where XState throws. A fallible function returns a result<T>, holding either its value or a boost::system::error_code: an xstate::errc, of the category webcpp.xstate, or xactor’s errc::invalid_argument, of the category webcpp.xactor, which xstate::errc does not have. actor_system returns invalid_argument for a start, a send, a stop, a status_of, a subscribe or an on_emitted whose address names no actor, for the host’s stop of a child, for an answer to a request no longer pending, and for a call from a host callback that changes the system; create_actor refuses a systemId another running actor holds with xstate::errc::system_id_taken. The const readers snapshot_of, child_of, parent_of, id_of and get refuse nothing: they answer a null pointer, none or an empty view for what they do not find. Nothing throws, since xstate builds without exceptions. A config XState would refuse is refused by create_machine, and an implementation that fails leaves the snapshot in the status error, with its error code.

XState: an exception, or a snapshot whose status is 'error'.
xstate: xstate::result<T>, the codes of xstate::errc, such as invalid_config, unknown_target and system_id_taken, and snapshot::error.
See Errors as values, Failures are values and errc.

Error event

The event a parent receives when a child actor fails: xstate.error.actor.<child id>, with the child’s error and its actorId. The invoke’s onError takes it. When no transition takes it, the parent fails in turn: its status error, its error code actor_failed, and the child’s error in snapshot::error_value.

XState: the same event; an unhandled one puts the parent’s snapshot in the status 'error'.
xstate: the same, with the error code actor_failed.
See Done and error events.

Event

Something that happened, which a machine reacts to: a type, which selects the transitions, and a payload, which implementations read. Events come from the caller or another actor, from the machine itself (a raised event, a done event), or from a clock (a delayed event).

XState: an event object, { type: 'submit', ...payload }.
xstate: xstate::event (type, payload); event_from_json reads XState’s flat form and to_json(event) writes it.
See Events.

Event descriptor

The key of an on, which says the events its transitions take: an event type, the wildcard * for every event, or a prefix ending in .* (mouse.* takes mouse.click). A state tries the transitions of the exact type first, then those of the matching wildcard descriptors, the longest first.

XState: the event descriptor, in the same three forms.
xstate: the same; an event of the type * sent from outside fails its macrostep with invalid_event, as XState’s development build throws on it.
See Event descriptors.

Eventless transition

A transition no event triggers. After the event’s own microstep, and after every later one, the machine takes the always transitions its active states enable, before any raised event; each guard is evaluated with the last event taken. Only after a microstep of eventless transitions that changed nothing (no active node, history, context, status or child) does it skip them, and take the next raised event or settle. A guard usually decides where an eventless transition goes, as in a choice between two states.

XState: always.
xstate: always, selected as XState selects them. A cycle of them never settles, and only fuel bounds it.
See Eventless transitions.

Execution

Everything one input from the host causes: the messages and the timers that follow from it, through any number of actors, paid from one reservoir of fuel, the budget’s. In xactor, a delivery or a tick from the host opens one, and every message it causes belongs to it. In the actor layer, each host call that delivers something (start, send, clock_tick, resolve, reject, resume) opens executions of its own, each with the whole budget: one for each input it applies and, for resume, one for each parked actor it continues; their microsteps are paid from it too. So the budget bounds an execution, and one call may spend it more than once.

XState: none; XState has no fuel.
xactor: an execution named by a correlation_id, whose fuel fuel_of(correlation) tells; xactor::budgets, which xstate::budgets names, sizes it.
See Fuel, parking and resume, and an execution in xactor’s glossary.

Final state

A state that, once entered, completes its parent. When a final state at the machine’s top level is entered, the machine is done: its status becomes done, the children it has left are stopped, and, as an actor, it reports its output to its parent.

XState: type: 'final'.
xstate: "type": "final"; status::done and snapshot::output.
See Final states.

Fuel

The units a caller pays a machine’s work with, so that no event costs more than the caller allows. With the machine core, the caller pays for each next() of the cursor, and stops when it has no more, keeping the cursor to resume it later. In the actor layer, each execution has the budget’s fuel: every microstep costs one unit, and so does every message an actor sends and every timer it arms; an actor that cannot pay parks. A cycle of eventless transitions never settles, and fuel is its only bound.

XState: none. XState runs each macrostep to its end; the config’s options.maxIterations, when set, makes a macrostep throw once that many microsteps have followed the event’s own with the machine still active, even when it would settle there. xstate does not read that option.
xstate: the caller’s own count with the core; xstate::budgets::fuel in the actor layer, with run_outcome::out_of_fuel and resume().
See Fuel in Pure transitions, Fuel, parking and resume, Fuel in Where xstate differs, and the fuel example.

Guarantee

A rule a library keeps whatever its caller does, numbered: xstate’s, those of its actor layer numbered from A1, and xactor’s, on xactor’s page. A guarantee keeps its number and its anchor, and a comment of the code or of a test cites it by that anchor, as (doc: #xstate-invariant-13) does; the build fails on a citation that names no anchor of this page.

XState: none as a list; XState’s tests, which xstate’s cases port, pin its behaviour.
xstate: the anchors xstate-invariant-<n> and xstate-invariant-a<n>.
xactor: the anchors xactor-invariant-<n> of xactor’s page.
See Guarantees.

Guard

A condition that decides whether a transition is taken. Of the transitions an event selects in a state, the first whose guard passes is taken, and a transition without a guard always passes. A guard reads the context, the event and its params, and changes nothing. XState’s higher-order guards, not, and, or and stateIn, are functions in XState; xstate writes them as JSON.

XState: guard: 'isValid', implemented in setup({ guards }), and not(...), and([...]), or([...]), stateIn(...).
xstate: "guard": "isValid", implemented in implementations.guards as a predicate, and {"type": "xstate.not", "guards": [...]} and its siblings. A guard the implementations do not hold is refused at creation with unknown_guard.
See Guards, Higher-order guards and stateIn and Higher-order guards in Where xstate differs.

History state

A pseudo-state that remembers which children of its parent were active when the parent was left, and returns to them when a transition targets it. A shallow history remembers the parent’s children; a deep one, every descendant. Until it remembers something, it goes to its own target, or to the parent’s initial state.

XState: type: 'history' with history: 'shallow' or 'deep', or, on a state without type or children, a truthy history alone; snapshot.historyValue.
xstate: the same in the config, where a history of false, null, "" or 0 is falsy, as in JavaScript; snapshot::history, by the history state’s id.
See History states.

Host

The program that runs an actor system: it creates the root actors, starts them, sends them events, moves the time, answers the requests of the host actors and stops the roots. Its callbacks hear what the actors do, and each runs inside an actor’s turn: it may read the system, and a call from it that changes the system returns invalid_argument and changes nothing (listener).

XState: the code that calls createActor, actor.start() and actor.send().
xstate: the caller of the members create_actor, start, send, clock_tick, resolve, reject, resume and stop of actor_system.
See Creating an actor and Watching an actor.

Host actor

An actor whose work the host does, outside the machines: a fetch, a query, a computation. Once started it is a pending host request; the host answers it, and the actor is done with an output or fails with an error, which its parent hears as a done or an error event.

XState: a promise actor, fromPromise(...).
xstate: xstate::host_actor, registered in implementations.actors and answered with actor_system::resolve or reject.
See Host actors and the actor system example.

Host request

A started host actor waiting for its answer: the actor, its parent, its id, its src and its input. A request whose actor has stopped, because its state was left, is no longer pending, and answering it changes nothing.

XState: the call of a fromPromise creator, with its input.
xstate: xstate::host_request, listed by actor_system::host_requests(); resolve and reject refuse one no longer pending with xactor’s invalid_argument.

Implementations

The functions a config names, registered by name: actions, guards, delays, the actors an invoke or a spawn runs, the initial context, and the inputs and outputs a config cannot hold as values. A config is JSON and holds no function, so every function has a name in the config and a body here.

XState: setup({ actions, actors, guards, delays }), and the functions written in the config.
xstate: xstate::implementations, given to create_machine: the maps actions, actors, guards, delays, inputs (by invoke id) and outputs (by state id), and context.
See Implementations.

Initial state

The child a compound state enters first, which its initial names. A machine starts with its initial macrostep: its first microstep enters the initial states, and what follows from them runs as in any macrostep, with the event xstate.init, which carries a non-null input as its member input.

XState: initial; getInitialSnapshot and initialTransition.
xstate: initial; begin_initial(machine, input), and get_initial_snapshot and initial_transition, which run it to its end.
See Initial states and The initial macrostep.

Input

The data an actor starts with, given by its creator: the host for a root, the parent for a child. A machine reads it to make its initial context.

XState: createActor(machine, { input }), an invoke’s or a spawn’s input, and the context function ({ input }) => ....
xstate: actor_options::input and begin_initial(machine, input); an invoke’s input in the config or implementations.inputs, by invoke id; spawn_child_action::input.
See A machine’s input, Input to an invoked actor and Input to a spawned actor.

Inspector

Callbacks that hear what every actor of a system does, for tools and logs: each actor’s start, each macrostep a machine actor settles with the event that began it, and each event an actor emits. A macrostep that fails where XState’s transition throws, in an implementation or while an action resolves (a claim of a systemId another actor holds, a target that names no actor), is not heard: XState inspects no snapshot then. One that an unhandled error event fails, with actor_failed, is heard, and so is one that a deferred effect fails, with the snapshot the macrostep reached. Like a listener, an inspector runs inside an actor’s turn and may only read the system.

XState: the inspect option of createActor, with inspection events such as @xstate.actor, @xstate.snapshot and @xstate.event.
xstate: xstate::inspector (started, settled, emitted), given to actor_system::inspect.
See Inspection.

Interleaving

The order in which the deliveries of different actors run. Each actor receives what one sender sends it in the order sent, and runs each macrostep as XState does. Across actors the order may differ from XState’s, since xactor queues each delivery where XState delivers it to an idle actor at once: an actor that sends ping twice to a, then once to b, has b handle its event between the two that a handles, where XState runs the three in the order sent. In the same way, when an actor hears from two senders and one send is caused by the other’s, the two may arrive in another order than in XState, and the callbacks of different actors may run in another order too.

XState: none as a term; XState delivers an event to an idle actor at once, nested in its sender’s call.
xstate: xactor’s queues: each delivery waits in its receiver’s mailbox, and the actors with mail take turns, one message each (xactor).
See Interleaving across actors.

Invoke

A child actor that lives as long as a state is active: entering the state starts it, leaving the state stops it. The state’s onDone and onError take the child’s done and error events.

XState: invoke with src, id, input, systemId, onDone, onError and onSnapshot.
xstate: the same in the config, its src a name of implementations.actors; its onDone, onError and onSnapshot are transitions on xstate.done.actor.<id>, xstate.error.actor.<id> and xstate.snapshot.<id>, and the actor layer sends no snapshot event yet. Entering the state returns xstate.spawnChild and leaving it xstate.stopChild. An invoke without an id gets XState’s <index>.<state id>.
See Invoking actors.

Invoked actor

See invoke.

Listener

A callback the host gives an actor system to hear one kind of thing: the snapshots an actor settles in while it is active and the one it is done in, the events it emits, or the custom actions or the logs the actors hand over. A failed actor’s snapshot reaches no listener: status_of and snapshot_of tell the failure. A listener runs inside an actor’s turn. It may read the system, and a call from it that changes the system returns xactor’s invalid_argument and changes nothing, where XState accepts a send from a subscriber and handles it once the update in progress is done (A callback that calls the system back).

XState: actor.subscribe(...), whose error callback hears a failure, and actor.on(...); a running actor runs custom actions and logs itself.
xstate: actor_system::subscribe, on_emitted, on_action and on_log.
See Watching an actor.

Machine

A state machine: a finite set of states, the transitions between them and the actions they run. In xstate, the immutable tree of state nodes a config describes, with the implementations it names. It is created once and never changes, so one machine serves any number of snapshots and actors, and copying it is cheap.

XState: a StateMachine, from createMachine(config) or setup({ ... }).createMachine(config).
xstate: xstate::machine, from create_machine(config, implementations), which returns a result<machine>.
See Machines.

Machine actor

An actor that runs a machine: a root the host creates from a machine, or a child whose src names a machine in the implementations. It receives events, sends events, has children, starts from an input and ends with an output.

XState: the actor createActor(machine) returns, or a child whose src names a machine in setup({ actors }).
xstate: actor_system::create_actor(machine, options), and an xstate::machine_actor in implementations.actors.
See Machine actors.

Machine core

The half of xstate that computes what a machine does, and runs nothing: it creates machines, computes snapshots with XState’s pure functions and the macrostep cursor, holds delayed events in a simulated clock its caller owns, and writes a machine’s definition as JSON. It includes nothing of xactor and runs no actor. It knows XState’s actors as data, and resolves a spawn or a stop into an action for its caller, which the actor layer executes.

XState: its machines and pure functions, transition and the others, which XState’s package holds beside its actors.
xstate: <webcpp/xstate.hpp>, which needs Boost.JSON, Boost.System and the standard library.
See The machine core.

Macrostep

Everything one event does: the microstep of the transitions the event selects, then every microstep that follows, an eventless transition first and a raised event otherwise, until the machine is stable or no longer active. An actor runs one macrostep at a time.

XState: what transition() or actor.send() runs; macrostep is the name of its internal function, after SCXML.
xstate: xstate::macrostep, the cursor that runs one, and macrostep_result (snapshot, actions, microsteps), what it settled in.
See The macrostep cursor and the machine example.

Meta

Static data a state carries for whoever reads the machine, such as the text a view shows in that state: any JSON, which the machine never reads. A description is a string that documents a state for the people who read the machine. A transition may have both too.

XState: meta and description; snapshot.getMeta().
xstate: the same in the config; get_meta(machine, snapshot), the meta of every active node that has one, by node id; state_node::meta and state_node::description. The definition writes them, except a state’s null meta and a state’s description that is not a string, which xstate leaves out (A state’s null meta, and a description that is not a string).
See Meta and descriptions.

Microstep

One step of a macrostep: the transitions selected together, the states they exit and enter, and the actions those return, in one change of the snapshot. The first microstep of a machine enters its initial states.

XState: a [snapshot, actions] pair of getMicrosteps(), and the inspection event @xstate.microstep.
xstate: xstate::microstep (event, transitions, actions, snapshot), from get_microsteps or from one next() of the cursor.
See Microsteps.

Oracle

XState itself, run on xstate’s test cases: every case ported from XState’s tests, or written where they are silent, runs on XState 5.33.2’s development build, and what XState produces is committed as what xstate’s suites must produce, microstep by microstep and actor by actor.

XState: none.
xstate: test/oracle/oracle.mjs, which runs the machine cases, and test/oracle/actors.mjs, which runs the actor cases; b2 libs/xstate/test/oracle//oracle runs them again.
See XState as the oracle and XState as the oracle in Testing.

Output

What a final state or a machine produces when it is done. A final state’s output travels with its done event; the machine’s is its snapshot’s once it is done, and reaches its parent in its done event.

XState: output on a final state or on the machine; snapshot.output.
xstate: output in the config, or implementations.outputs by state id, the root’s id for the machine’s; snapshot::output.
See Output.

Parallel state

A state whose children, its regions, are all active at once, each in a state of its own. Its state value is an object with one key per region. It is done when every region is in a final state.

XState: type: 'parallel'.
xstate: "type": "parallel"; the order of snapshot::nodes, XState’s, decides which region’s eventless transition comes first.
See Parallel states.

Params

The JSON value a config gives one use of an action or a guard, beside its name: {"type": "addRating", "params": {"by": 5}}. Uses of one implementation tell themselves apart by their params, which the implementation reads.

XState: params, a value, or a function of the context and the event computed when the action runs.
xstate: params in the config, always a fixed value, since JSON holds no function; action_args::params, null when the config gives none, and action::params on a returned action.
See Action objects and params.

Parent and child states

A state with states nested in it is their parent, a compound state. It is active while one of its children is, and it enters first the child its initial names. A compound state without initial is refused at creation.

XState: compound states, with states and initial.
xstate: the same; node_type::compound.
See Parent states.

Parked actor

An actor whose execution ran out of fuel in the middle of its work. It keeps what it has not done, its cursor among it, and does nothing until resume() gives it a new execution. The snapshot it publishes is still the last one it settled in. Events that reach it meanwhile wait behind its work. While any actor is parked, the host’s start, send, clock_tick, resolve and reject are checked, held in the order made, and return out_of_fuel; resume() applies them once the parked actors have settled. stop, create_actor, resume, the registrations of callbacks and the queries run at once.

XState: none.
xstate: a host call that leaves one parked returns run_outcome::out_of_fuel; actor_system::resume() continues it.
See Fuel, parking and resume and the fuel example.

Persisted state

A snapshot kept outside the program between two events, in a file or a database, and turned back into a snapshot when the next event comes. xstate keeps the state value and the context: resolve_state completes the value to every node it implies, with the status done when the value puts the machine in a final state and active otherwise, never error. The output, the error, what history states remember and the children are not restored.

XState: machine.resolveState({ value, context }), often of what JSON.stringify(snapshot) wrote; and an actor’s getPersistedSnapshot(), which createActor(machine, { snapshot }) restores with what its history states remember.
xstate: resolve_state(machine, value, context), which returns a result<snapshot> and refuses a value that names no state with unknown_state. xstate has no counterpart of an actor’s persisted snapshot: what history states remember lives only in an xstate::snapshot.
See Persisted state.

Pure transition function

A function that computes what a machine would do, without starting an actor and without running any effect: the next snapshot, and the actions to run.

XState: transition, initialTransition, getNextSnapshot, getInitialSnapshot, getMicrosteps, getInitialMicrosteps; when a step invokes or spawns, the snapshot they return holds the child as an actor, created and not started.
xstate: transition, initial_transition, get_next_snapshot, get_initial_snapshot, get_microsteps, get_initial_microsteps, each the cursor run to its end, without a bound; a snapshot holds a child as data, its id and its src.
See Computing the next state.

Raised event

An event a machine sends itself. Without a delay, it goes to the macrostep’s internal queue and is taken, in a microstep of its own, once no eventless transition is enabled, before the macrostep settles: no other event comes in between. With a delay, it is a delayed event.

XState: raise(event).
xstate: raise_action, returned as xstate.raise; the internal queue is part of the cursor, so a copy of the cursor has it too.
See Raise.

Region

One child of a parallel state, active with all its siblings. See parallel state.

Root actor

An actor the host created, which has no parent. The host stops it, and its whole family with it; a child is stopped only by its parent. A root created without an id is named x:<n>, as XState names an actor by its session id.

XState: the actor createActor returns, actor.stop() and actor.sessionId.
xstate: actor_system::create_actor and stop(root); parent_of holds nothing for it, and id_of gives its id.
See Creating an actor and Options.

Run to completion

The rule that an actor handles an event entirely, its whole macrostep, before it takes the next one. Events that arrive meanwhile wait, in the order they arrived.

XState: the same rule, as SCXML states it.
xstate: the same for every actor, a macrostep parked for fuel included.
See Messages and mailboxes and How the layer runs.

Self-transition

A transition that stays in its source state: one without a target, one whose target is the source, or one that re-enters the source, its target the source and reenter: true. Only the re-entering kind leaves the source, so only it runs the source’s exit and entry actions and restarts its invoked actors. One whose target is the source leaves the source’s children and enters them again at their initial states; one without a target leaves nothing.

XState: the same three kinds.
xstate: the same; transition_definition::target and reenter.
See Self-transitions and reenter.

Send

The built-in actions that send an event to another actor: sendTo, sendParent, and forwardTo, which sends on the event being handled. A target is a string: a child’s id, or #_ and the id; #_parent; #_internal, the actor itself; or #system: and a systemId.

XState: sendTo(target, event, options), sendParent(event) and forwardTo(target); a target may be a function, such as ({ system }) => system.get(id).
xstate: send_to_action, send_parent_action and forward_to_action, each returned as xstate.sendTo; a function cannot be JSON, so a systemId is named as #system:<systemId>.
See Sending to other actors and Sending events between actors.

Settled

Said of a macrostep that has nothing left to do: no eventless transition is enabled and no raised event is queued, or the machine is no longer active. The cursor says so on the microstep that settles it, and the snapshot that microstep leaves is the new stable one. An event that selects no transition changes nothing in its own microstep, and the macrostep settles there unless an eventless transition is then enabled, as one whose guard reads that event can be: the eventless transitions are tried after the event’s own microstep, even an empty one.

XState: none; XState’s loop ends there.
xstate: progress::settled and macrostep::done(); in the actor layer, run_outcome::settled says that no actor was left parked.
See One microstep at a time.

Snapshot

Where a machine is, as a value: its state value, its context, its status, its output or its error, its active state nodes, what its history states remember, its tags and its children. A macrostep starts from one snapshot and settles in another; none changes in place. A snapshot can be kept, copied and stored.

XState: a MachineSnapshot, from getInitialSnapshot, transition or actor.getSnapshot().
xstate: xstate::snapshot; in the actor layer, actor_system::snapshot_of(actor), which an actor publishes once its macrostep settles.
See The snapshot and Snapshots and status.

Spawn

Starting a child actor with an action rather than with a state: the child lives until a stopChild stops it, or until its parent is done or stopped.

XState: spawnChild(src, { id, systemId, input }), and spawn inside an assign.
xstate: spawn_child_action (src, id, system_id, input), returned as xstate.spawnChild; a spawn without an id keys the child under "", and xstate has no spawn inside an assign yet.
See Spawning actors.

State

A mode a machine can be in, which decides how it reacts to events: a light is green, yellow or red. A machine is in one state at a time, or in one state of each region of its parallel states.

XState: a state node of the config.
xstate: a key of states in the config, held as a state node.
See Finite states.

State machine

See machine.

State node

One node of a machine’s tree: the root, or an atomic, compound, parallel, final or history state, with its transitions, actions, invokes and tags. Its id is the config’s id, or the machine’s id and the node’s path joined by dots (light.green), which a target names with a #.

XState: a StateNode; machine.getStateNodeById(id).
xstate: xstate::state_node, named by its index, its place in document order; machine::node(index) and machine::node_by_id(id).
See State nodes and ids.

State value

A snapshot’s description of its active states, as JSON: a string when the machine’s active state has no children ("green"), an object for nested and parallel states ({"red": "walk"}). A snapshot matches a state value when it is in that state, or in a state within it.

XState: snapshot.value, snapshot.matches(value) and matchesState.
xstate: snapshot::value, a boost::json::value; snapshot::matches and matches_state.
See The state value and Matching a state value.

Statechart

A state machine extended with hierarchy (parent and child states), concurrency (parallel states), history, and communication with other actors. XState’s machines are statecharts, run by the algorithm SCXML defines, and so are xstate’s, which port that algorithm from XState.

XState: every machine.
xstate: every machine.
See State machines and statecharts.

Status

Whether a machine still runs: active; done, once it is complete; or error, once a microstep failed (an implementation failed, a sendTo named a child that is not there, a machine whose root is itself a history state needed its default, an event of the type * came from outside) or a child’s error went unhandled. In the actor layer, an actor also fails where XState throws while it resolves an action: a claim of a systemId another actor holds, a sendTo to the parent of a root. An actor of the actor layer also has xactor’s status, which adds stopped.

XState: snapshot.status: 'active', 'done', 'error' or 'stopped'.
xstate: snapshot::status, an xstate::status; actor_system::status_of, an xactor::status.

systemId

A name an actor is registered under in its actor system, so that any actor of the system reaches it without a chain of parents. It names one actor at a time, from the actor’s creation until it is done, fails or is stopped. A claim of a name another actor holds fails the claiming actor with system_id_taken, and create_actor refuses it.

XState: systemId on an invoke, a spawn or createActor, and system.get(systemId).
xstate: systemId on an invoke, spawn_child_action::system_id and actor_options::system_id; actor_system::get(system_id); the target #system:<systemId>.
See The system and the actor system example.

Tag

A label on a state, which a snapshot carries while that state is active. Testing a tag, such as busy, keeps working when the states that mean busy change; testing each of those states does not.

XState: tags, and snapshot.hasTag(tag).
xstate: tags in the config; snapshot::tags and snapshot::has_tag(tag).
See Tags.

Target

Where a transition goes: a sibling state’s key, a path below the state starting with a dot (.child), or any state by its id (#light.green); a list of targets enters several regions. A transition without a target stays in its state and runs its actions. A target that names no state is refused at creation with unknown_target.

XState: target.
xstate: target; transition_definition::target, the indexes of the nodes it names.
See Targets and Multiple targets.

Timer

See clock.

Transition

The move from a state to another that an event causes, with the actions it runs on the way. Transitions are deterministic: a state, an event and a context always lead to the same next state. A transition whose targets are its source or inside it does not leave the source; with reenter: true it leaves the source and enters it again, so the source’s exit and entry actions run and its invoked actors restart. reenter changes nothing for a transition without a target, and a transition to another state leaves its source either way.

XState: on: { event: 'target' }, or target, guard, actions and reenter.
xstate: the same in the config; xstate::transition_definition, which a microstep’s transitions point to.
See Transitions and Self-transitions and reenter.

Twin

The program that does on XState 5.33.2 what a program of this page does on xstate, and prints what it finds the same way. A twin agrees, and prints its program’s output; or it diverges on purpose, and prints what XState does where xstate differs, which Appendix B shows after the program’s output. A program that shows what XState has no counterpart for has no twin, and says why.

XState: none.
xstate: test/oracle/twins/<group>/<name>.mjs, beside a divergent twin’s output, <name>.expected; webcpp’s tools/oracle/twins.py runs them and compares.
See A twin for every example and The XState twins.

Undefined

JavaScript’s value for what is not there, which JSON cannot hold. Where an XState implementation may return undefined, a log’s value, a child’s input or a state’s output, xstate’s returns a std::optional with none, and the member is left out, as XState leaves it out. The event xstate.init carries input only when the input is not null.

XState: undefined.
xstate: none in the result of a value_maker, in host_request::input and in snapshot::output; a null pointer for the value on_log receives.
See undefined in Where xstate differs and When a value is undefined.

Wildcard

See event descriptor.

xactor

The actor system xstate’s actor layer runs on, header-only and single-threaded. Its scheduler holds one first-in first-out mailbox per actor and one first-in first-out queue of the actors that have mail: each turn hands one message to the actor at the front of the queue, which goes back to the end of the queue while its mailbox holds more. A delivery is queued, never nested in its sender’s call, so the same inputs give the same run. Each execution has its fuel, and timers run on a clock the host moves. xactor is a library of webcpp of its own, and knows nothing of xstate. xstate’s machine core includes nothing of xactor and runs no actor: it knows XState’s actors as data, and resolves invokes and spawns into xstate.spawnChild and xstate.stopChild actions for its caller.

XState: none; XState’s actors run on JavaScript’s event loop.
xactor: webcpp::xactor, which <webcpp/xactor.hpp> includes whole; the actor layer’s headers, gathered by <webcpp/xstate/actors.hpp>, include it.
See xactor’s page, The actor layer and How the layer runs.

XState

Stately’s JavaScript library of state machines and actors, which xstate ports at version 5.33.2. Where this page names XState, it means that version. Its npm package, xstate, pinned at 5.33.2 under test/oracle/, is what the oracle and the twins run, on its development build, as XState’s own tests do.

xstate

The port of XState’s machines and actors: a header-only C++20 library that loads a machine from the JSON config XState’s createMachine takes, and runs it through XState’s pure functions or as an actor. It has two layers, the machine core and the actor layer. It is one of the libraries of webcpp, beside xactor.

XState: the library it ports.
xstate: webcpp::xstate, under include/webcpp/xstate/.
See Overview and Two layers.

Names that start with xstate.

The library gives these names to the events it makes, to the actions it returns and to the higher-order guards. A config may handle the events like any other, and a caller reads the type and the params of each action.

Name What it is Entry

xstate.init

The event of a machine’s initial macrostep, with a non-null input as its member input.

Initial state

xstate.after.<delay>.<state id>

The event a state’s after raises with that delay.

Delayed transition

xstate.done.state.<state id>

Raised when that compound state’s final child is entered, with the child’s output, or when every region of that parallel state is final.

Done event

xstate.done.actor.<id>

Sent to the parent when its child id is done: output, actorId.

Done event

xstate.error.actor.<id>

Sent to the parent when its child id fails: error, actorId.

Error event

xstate.snapshot.<id>

The event an invoke’s onSnapshot takes. The actor layer sends none yet, so such a transition is taken only for an event of that type a caller sends.

Invoke

xstate.raise

A raise, returned with the params event, and id and delay when it has them.

Raised event

xstate.sendTo

A sendTo, a sendParent or a forwardTo, with targetId, event, and id and delay when it has them.

Send

xstate.cancel

A cancel, with the sendId it drops.

Delayed event

xstate.log

A log, with its value and label when it has them.

Built-in action

xstate.spawnChild

A spawn, or an invoke’s start, with id, src, and input and systemId when it has them.

Spawn, Invoke

xstate.stopChild

A stopChild, or an invoke’s stop, with the id of the child.

Child actor

xstate.emit

An emit, with the event emitted.

Emitted event

xstate.not, xstate.and, xstate.or, xstate.stateIn

The JSON of XState’s higher-order guards, which XState writes as functions.

Guard

The terms in a machine

An order: add counts an item, and checkout goes to checking, whose eventless transitions go on to paying when the guard hasItems passes and back to cart otherwise. paying names a custom action, charge, and raises paid, which enters the final state closed. The program drives each macrostep with the cursor, one microstep per next():

const boost::json::value config = boost::json::parse(R"({
    "id": "order",
    "initial": "cart",
    "context": {"items": 0},
    "states": {
        "cart": {
            "on": {
                "add": {"actions": "count"},
                "checkout": {"target": "checking"}
            }
        },
        "checking": {
            "always": [{"target": "paying", "guard": "hasItems"}, {"target": "cart"}]
        },
        "paying": {"entry": ["charge", "confirm"], "on": {"paid": "closed"}},
        "closed": {"type": "final"}
    }
})");

const xstate::assigner count =
    [](const xstate::action_args& args) -> xstate::result<boost::json::object> {
    const std::optional<std::int64_t> items = items_of(args.context);
    if (!items.has_value()) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"items", *items + 1}};
};
const xstate::event_maker paid =
    [](const xstate::action_args&) -> xstate::result<xstate::event> {
    return xstate::event{.type = "paid", .payload = {}};
};
const xstate::predicate has_items =
    [](const xstate::action_args& args) -> xstate::result<bool> {
    const std::optional<std::int64_t> items = items_of(args.context);
    if (!items.has_value()) {
        return xstate::failure<bool>(xstate::errc::implementation_failed);
    }
    return *items > 0;
};

xstate::implementations implementations;
implementations.actions.emplace("count", xstate::assign_action{.assignment = count});
implementations.actions.emplace(
    "confirm", xstate::raise_action{.event = paid, .id = std::nullopt, .delay = std::nullopt});
implementations.guards.emplace("hasItems", has_items);

const xstate::result<xstate::machine> order = xstate::create_machine(config, implementations);
if (!order.has_value()) {
    return 1;
}

xstate::snapshot now = xstate::get_initial_snapshot(*order);
std::cout << "initial: " << boost::json::serialize(now.value) << ' '
          << boost::json::serialize(now.context) << ' ' << status_name(now.status) << '\n';
for (const std::string_view type : {"checkout", "add", "checkout"}) {
    xstate::macrostep step =
        xstate::begin(*order, now, xstate::event{.type = std::string(type), .payload = {}});
    std::size_t number = 0;
    while (!step.done()) {
        const xstate::progress made = step.next();
        print(type, ++number, made);
    }
    now = step.result().snapshot;
}

Its helpers: items_of reads the context’s items, status_name names a status, and print writes a microstep from the progress that next() returned, its step.snapshot, step.actions and settled.

/** The count of items an order's context holds, or none when it holds no integer `items`. */
std::optional<std::int64_t> items_of(const boost::json::value& context) {
    const boost::json::object* members = context.if_object();
    const boost::json::value* items = members == nullptr ? nullptr : members->if_contains("items");
    if (items == nullptr || !items->is_int64()) {
        return std::nullopt;
    }
    return items->get_int64();
}

std::string_view status_name(xstate::status status) {
    switch (status) {
        case xstate::status::active: return "active";
        case xstate::status::done: return "done";
        case xstate::status::error: return "error";
    }
    return "unknown";
}

/** Prints where a microstep left the machine and the actions it returned. */
void print(std::string_view label, std::size_t number, const xstate::progress& made) {
    const xstate::snapshot& snapshot = made.step.snapshot;
    std::cout << label << " #" << number << ": " << boost::json::serialize(snapshot.value) << ' '
              << boost::json::serialize(snapshot.context) << ' ' << status_name(snapshot.status)
              << " [";
    std::string_view separator;
    for (const xstate::action& returned : made.step.actions) {
        std::cout << separator << returned.type;
        separator = ", ";
    }
    std::cout << ']' << (made.settled ? " settled" : "") << '\n';
}

It prints:

initial: "cart" {"items":0} active
checkout #1: "checking" {"items":0} active []
checkout #2: "cart" {"items":0} active [] settled
add #1: "cart" {"items":1} active [] settled
checkout #1: "checking" {"items":1} active []
checkout #2: "paying" {"items":1} active [charge, xstate.raise]
checkout #3: "closed" {"items":1} done [] settled

Each line is a microstep, numbered within its event’s macrostep, with the state value, the context and the status it left, and the actions it returned.

  • The first checkout is a macrostep of two microsteps: #1 takes the event’s transition to checking, #2 the eventless transition back to cart, since hasItems is false. Nothing follows, so #2 settles it.

  • add changes the context through the assign count, and returns no action: an assign is never handed back.

  • The second checkout takes three microsteps. #2 is the eventless transition to paying, whose entry returns the custom action charge, for the caller to run, and xstate.raise, the raise of paid, already queued. #3 takes the raised event to closed, a final state: the status is done.

  • settled is what the cursor reports with the last microstep of each macrostep. XState has no cursor: its getMicrosteps returns every microstep of a macrostep at once, and the program’s twin marks the last one, so that it prints the same lines (twin).

  • count and hasItems read items through items_of: on a context without an integer items, each fails with implementation_failed, and the snapshot carries the error.

The terms in an actor system

An app that invokes a host actor to fetch its user, registered under the systemId fetcher. The host finds the request, answers it, and the app’s onDone keeps the output. The fetch is the host’s work: a host actor, which waits until the host answers it, stands where XState runs a promise (host actor):

const boost::json::value config = boost::json::parse(R"({
    "id": "app",
    "initial": "loading",
    "context": {"user": null},
    "states": {
        "loading": {
            "invoke": {
                "id": "fetch",
                "src": "fetchUser",
                "systemId": "fetcher",
                "input": {"userId": 42},
                "onDone": {"target": "ready", "actions": "keepUser"}
            }
        },
        "ready": {}
    }
})");

const xstate::assigner keep_user =
    [](const xstate::action_args& args) -> xstate::result<boost::json::object> {
    const boost::json::value* output = args.event.payload.if_contains("output");
    if (output == nullptr) {
        return xstate::failure<boost::json::object>(xstate::errc::implementation_failed);
    }
    return boost::json::object{{"user", *output}};
};

xstate::implementations implementations;
implementations.actors.emplace("fetchUser", xstate::host_actor{});
implementations.actions.emplace("keepUser", xstate::assign_action{.assignment = keep_user});

const xstate::result<xstate::machine> app = xstate::create_machine(config, implementations);
if (!app.has_value()) {
    return 1;
}

xstate::actor_system system({.fuel = 10'000});
const xstate::result<xstate::actor_ref> actor = system.create_actor(*app);
if (!actor.has_value() || !system.start(*actor).has_value()) {
    return 1;
}
print(system, *actor);
for (const xstate::host_request& request : system.host_requests()) {
    std::cout << "request: " << request.id << ' ' << request.src << ' '
              << boost::json::serialize(request.input.value_or(nullptr)) << '\n';
    std::cout << "fetcher is the request's actor: " << (system.get("fetcher") == request.actor)
              << '\n';
    if (!system.resolve(request, boost::json::object{{"name", "Ana"}}).has_value()) {
        return 1;
    }
}
print(system, *actor);
std::cout << "fetcher is registered: " << system.get("fetcher").has_value() << '\n';

print reads the snapshot the app published, snapshot_of, and its children, the (id, src) of each child it has:

/** Prints the actor's state value, its context and the ids of its children. */
void print(const xstate::actor_system& system, xstate::actor_ref actor) {
    const xstate::snapshot* snapshot = system.snapshot_of(actor);
    if (snapshot == nullptr) {
        return;
    }
    std::cout << "app: " << boost::json::serialize(snapshot->value) << ' '
              << boost::json::serialize(snapshot->context) << " children [";
    std::string_view separator;
    for (const auto& [id, src] : snapshot->children) {
        std::cout << separator << id;
        separator = ", ";
    }
    std::cout << "]\n";
}

It prints:

app: "loading" {"user":null} children [fetch]
request: fetch fetchUser {"userId":42}
fetcher is the request's actor: true
app: "ready" {"user":{"name":"Ana"}} children []
fetcher is registered: false
  • Starting the root actor runs its initial macrostep: the app enters loading, whose invoke starts the child fetch. The child is a host actor, so it becomes a host request with its id, its src and its input.

  • system.get("fetcher") finds the child by its systemId, from anywhere in the system.

  • resolve answers the request: the child is done, and the app receives its done event, xstate.done.actor.fetch, whose output the assign keepUser keeps. Leaving loading stops the invoke, and the child, done, no longer holds its systemId.

Fuel

XState has no fuel, so this program has no twin. The event go takes three microsteps, through two eventless transitions, and each execution has two units of fuel. Creating the actor opens no execution; start, send and resume each open one here.

const boost::json::value config = boost::json::parse(R"({
    "id": "steps",
    "initial": "idle",
    "states": {
        "idle": {"on": {"go": "one"}},
        "one": {"always": "two"},
        "two": {"always": "three"},
        "three": {}
    }
})");
const xstate::result<xstate::machine> steps = xstate::create_machine(config, {});
if (!steps.has_value()) {
    return 1;
}

// Each execution has two units of fuel; start, send and resume each open one, and `go` takes
// three microsteps.
xstate::actor_system system({.fuel = 2});
const xstate::result<xstate::actor_ref> actor = system.create_actor(*steps);
if (!actor.has_value()) {
    return 1;
}
print("start", system.start(*actor), system, *actor);
print("send go", system.send(*actor, xstate::event{.type = "go", .payload = {}}), system,
      *actor);
print("resume", system.resume(), system, *actor);

print tells how each host call ended, from its run_outcome, and shows the snapshot the actor has published:

/** Prints how a host call ended and the snapshot the actor has published. */
void print(std::string_view call, const xstate::result<xstate::run_outcome>& outcome,
           const xstate::actor_system& system, xstate::actor_ref actor) {
    if (!outcome.has_value()) {
        std::cout << call << ": " << outcome.error().message() << '\n';
        return;
    }
    const xstate::snapshot* published = system.snapshot_of(actor);
    std::cout << call << ": "
              << (*outcome == xstate::run_outcome::settled ? "settled" : "out_of_fuel") << ", "
              << (published == nullptr ? "no snapshot" : boost::json::serialize(published->value))
              << '\n';
}

It prints:

start: settled, "idle"
send go: out_of_fuel, "idle"
resume: settled, "three"

The start pays for the one microstep of the initial macrostep. send pays for two of the three microsteps of go, then the actor parks: the call returns out_of_fuel, and the snapshot the actor publishes is still the one it last settled in, "idle". resume gives it a new execution, which pays for the third microstep; the macrostep settles, and the actor publishes "three".

Appendix E: Changelog

What changed in xstate in each version of webcpp, newest first, one line per change. "Unreleased" is what the next version brings. The release notes of every version are on webcpp’s index page.

Unreleased

webcpp 0.1.0

  • Changed the guarantee without exceptions to compiling only, which the lint checks.

  • Added the WEBCPP_XSTATE_NO_EXCEPTIONS configuration macro, defined automatically or by the developer.

  • Added a changelog, the page’s last appendix, with each version’s changes.

  • Changed config.hpp to include <webcpp/config.hpp>, webcpp’s configuration, which the build adds.

  • Added WEBCPP_NO_EXCEPTIONS, which disables xstate’s exceptions with every webcpp library’s.