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
assigntakes a function that computes the new count; here the config names an action,increment, andimplementationsbinds that name to C++ code, anassign_action, as XState’ssetupregisters an action under a name. This is machines as JSON, implementations by name. -
create_machinereturns aresult: 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 aresulttoo; whenaddingfinds no integer count, its failure puts the actor in the statuserror, as an exception thrown by an assigner does in XState. This is errors as values. -
The actor lives in an
actor_systemthe program creates. XState’screateActormakes an actor and the system that runs it in one call; in xstate,create_actormakes an actor in a system, andstart,sendandsubscribeare 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 eachstartandsendopens 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,transitionand 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
transitiondoes, 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 |
|
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 |
|
|
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 withsetupis referenced by its name, withparams, 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.txtgives 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,fromObservableandfromEventObservable. -
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, andresolve_state, XState’smachine.resolveState, brings it back; Pure transitions and the macrostep cursor shows it. -
toPromiseandwaitFor. -
syncSnapshot, and with it an invoke’sonSnapshot: the config acceptsonSnapshot, but the actor layer sends no child’s snapshot to its parent, so no child’s snapshot ever takes that transition. -
spawninsideassign. -
machine.provide(): a machine takes its implementations once, fromcreate_machine, and the same config created again with other implementations is the machineprovidewould 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/includeandlibs/xactor/includein 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/includein 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 istools/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 namesboost::throw_exception, and the program defines it: webcpp’stools/throw_exception.cppdoes 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.
startandsend, like every call that delivers something, run the system until nothing is left to do before they return, as XState’ssendprocesses the event at once, unless an actor is parked: then the call is held, in order, untilresume(). Each returns aresult<run_outcome>:settledwhen no actor is left parked,out_of_fuelwhen one is, or an error, such as xactor’sinvalid_argument, of the categorywebcpp.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; astartor asendopens 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 returnsout_of_fueland 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’ssettledtakes an error andout_of_fuelalike as a failure, and returns 1. -
Start.
startconstructs the actor and runs its initial macrostep, sosnapshot_ofreturnsnullptruntil then. XState’screateActoralready constructs it, and itsgetSnapshot()answers beforestart()(The root is constructed onstart). -
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_offor one, but a call from it that changes the system, asendincluded, returnsinvalid_argumentand 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
createMachinetakes, 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 |
|---|---|
|
the state value, a string or an object of the active states |
|
the context |
|
|
|
the output, once the machine is done |
|
why the machine failed |
|
the tags of the active states, each once |
|
the active state nodes, by their place in the machine, in XState’s order |
|
what each history state remembers |
|
the child actors, each as its id and its |
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 |
|---|---|---|
|
The state’s id; on the root, the machine’s id |
|
|
The child states, by key, and the child a compound state starts in: its
key, or |
|
|
|
|
|
A history state’s depth, deep for |
|
|
On the root: the initial context. A null one, or none, is |
|
|
The transitions, by event type |
|
|
The eventless transitions, and the delayed ones by delay |
|
|
The transition a compound or parallel state takes when it is done |
|
|
The actions run when the state is entered and when it is left |
|
|
The actors the state runs while it is active, |
|
|
On a final state, its output; on the root, the machine’s |
|
|
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 |
|
|
On the root: the machine’s version, which its definition repeats on every node; a machine loaded from a definition has the definition’s |
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_jsonwrites 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
afterdelay 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 itsdelayis 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 |
|---|---|---|---|
|
action name |
XState’s |
|
|
guard name |
XState’s |
|
|
delay name |
XState’s |
|
|
|
XState’s |
|
|
(one function) |
XState’s |
|
|
invoke id |
An invoke’s |
|
|
state id |
A final state’s |
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 withinvalid_config: calling it would abort in a build without exceptions. An optional function left out holds none: alog_actionwithout avalueis a log of the context and the event, as in XState. -
An
inputsentry names an invoke whose config gives noinput, and anoutputsentry a final state, or the root, whose config gives nooutput; any other isinvalid_config. -
Every actor the config names, by an invoke’s
srcor through aspawn_child_actionit names as an action, is an entry ofactors; any other isunknown_actor. A registeredspawn_child_actionthe config never names is not checked, as XState’ssetupaccepts 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
|
|
Accepts each of these at creation: a list is a machine with no state, a
non-string |
Has an |
|
Reads it, as its |
Has a compound state without |
|
Throws in |
Has an |
|
Throws in |
Has a transition that declares a truthy |
|
Throws in |
Has a transition target that names no state |
|
Throws in |
Has a history state, below the root, whose |
|
Throws when the history state is entered |
Has an |
|
Throws when the state is entered, the root’s when the machine starts |
Has a higher-order guard without a |
|
No counterpart: these are functions in XState |
Names a guard the implementations do not hold |
|
Throws when the guard is evaluated |
Has an |
|
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 |
|
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 |
|
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 |
|
Overflows its stack when it enters the state |
Has an invoke whose |
|
Takes inline actor logic, which JSON cannot hold |
Implementations: one holds an empty function |
|
No counterpart |
Implementations: an |
|
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’sgetStateNodeById. The#is optional, and a path after the id walks down from that node:#active.fastis the childfastof the node whose id isactive. -
child(node, key)is the child ofnodewhose key iskey, a dot being part of the key. -
node_by_path(from, path)is XState’sgetStateNodeByPath: a path of keys relative tofrom, 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 |
|---|---|---|
|
The state value: which states are active (The state value). xstate
computes it from |
|
|
The context, a |
|
|
|
|
|
The machine’s output once it is done, when it has one; none otherwise, where
XState’s is |
|
|
Why the machine failed, when |
|
|
When |
|
|
The active state nodes, root included, by their index in the machine, in XState’s order. |
|
|
What each history state remembers, by the history node’s id (History states). |
|
|
The tags of the active nodes, each once, in the order of |
|
|
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’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
targetthat is an ancestor whose initial chain reaches the history state again, would never finish entering:create_machinerefuses it withinvalid_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 aTypeErroras itserror(A config is refused when it is created, Failures are values). -
resolve_statebuilds 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
spawnandself; an xstate context cannot spawn a child (The context function). -
An empty
std::functionis refused bycreate_machinewithinvalid_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.assignis: 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_machinerefuses anassign_actionwhoseassignmentis empty, withinvalid_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
alwaysguarded 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 |
|---|---|
|
|
|
|
|
the same, reading its |
|
|
|
|
|
an |
|
an |
|
|
|
|
|
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,descriptionandmeta; -
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:
-
Start at each active atomic state, a state with no children.
-
Take the first of its transitions for the event that is enabled: one without a guard, or whose guard passes.
-
If it has none, look in its parent, and so on up to the root.
-
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 |
|---|---|
|
the sibling |
|
the child |
|
the child |
|
the state whose id is |
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 |
kept as they are |
not run |
keep running |
targeted, the source as its |
left and entered at their initial states |
not run |
keep running |
re-entering, a |
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.thatReentersgoes fromparentStateto its childotherChildState, and re-enters:parentStateis left and entered too. -
event.targetlessrunsnotifyand nothing else;otherChildStatestays. -
event.selftargetsparentStateitself: its child is left and its initial child entered, and the entry and exit actions ofparentStateitself do not run. -
event.normaltargets.someChildState, the active child: the child is inside the domain, so it is left and entered again. -
event.selfReentersre-entersparentStateand 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.*matchesfeedback,feedback.goodandfeedback.bad.rude, and notfeedbacks.
* 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
errorand 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
stateInwhose#idnames no state is accepted when the machine is created; evaluating it fails the macrostep withunknown_state, where XState’sgetStateNodeByIdthrows. 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
assignran), the children (a spawn or a stop) or the status. XState compares snapshot objects; xstate states the rule. So an unguarded targetlessalwayswhose 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_microstepsreport 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, adelay_maker: a function of theaction_argsthat returns the milliseconds, XState’ssetup({ delays }). It is computed each time the state is entered, from the context of that moment. XState’sdelaysalso maps a name to a number; in xstate a constant named delay is adelay_makerthat 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,emitand 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 giveson_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:
-
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.stopChildfor each child it invokes; -
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); -
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
initialis an object withactions, those actions. A compound state is entered by default when no target of the transition is inside it, and it then enters itsinitialchild.
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):
|
The context as it is when the action runs: an |
|
The microstep’s event. |
|
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
sendParentor a target#_parent, XState’stransition()throws; xstate returns anxstate.sendToto#_parent(sendParentwithout 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, aforwardTothrows in XState’s development build (The checks of XState’s development build), and asendTogoes to the inert actor itself, with notargetId. xstate returns anxstate.sendTowhosetargetIdis#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 |
|---|---|---|---|
|
|
never: the context changes |
- |
|
|
|
nothing more |
|
|
|
arms a timer |
|
|
|
cancels the timer |
|
|
|
delivers the event once the macrostep has settled, or arms a timer |
|
|
|
as a sendTo |
|
|
|
as a sendTo |
|
|
|
hands it to |
|
|
|
hands the event to the actor’s listeners once the macrostep has settled |
|
|
|
creates the child (Spawning actors) |
|
|
|
stops the child (Stopping a child) |
a function of your program |
nothing |
|
hands it to |
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:
transitionandinitial_transitionreturn all of them, and the cursor returns each microstep’s in itsmicrostep::actions, whichmacrostep_result::actionsgathers. 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_actionas 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 reachon_actionwhen the actor starts, as XState runs them onstart(). -
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’sstart()runs none for an actor born failed. -
on_actionholds 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, asendor aresolveamong them, returns xactor’sinvalid_argumentand 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_actionwith 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, anemit_actionwhoseevent_makerbuilds the event the host’s listener reads, or alog_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.assigndoes: the members it returns are replaced or added, and the others kept, asunitis. 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
constreference 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 withimplementation_failed. XState startsfalse,0and""as{}too; an assign dropstrueor 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 ofaddValuereturnsimplementation_failedwhen the event has no integervalue. -
This machine has no states: its root takes the event
incrementwith a transition that has actions and no target, and the actionaddValuehandles 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’sSimulatedClock.incrementtakes 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_clockis the caller’s clock: itsapplyschedules each built-in delayedxstate.raisea step returns and applies each built-inxstate.cancel, and ignores a custom action whatever its type,action::builtintelling them apart (Built-in and custom actions). Itspop_duegives 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_actionof the same actor by its id too (Sending to other actors). -
A state’s
afteris built of these two actions: entering the state raisesxstate.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 ofafter, the same as forstates(Nested states and the root): anafterthat writes"1000"before"500"raises the 500 one first. -
Time is whole milliseconds; XState’s
SimulatedClockalso 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:
|
The child of that id, an invoke’s or a spawned child’s. |
|
The same child, once it is in the snapshot’s children. |
|
The actor’s parent: |
|
The actor itself, which receives the event as one from outside, unlike a raised event. |
|
The actor registered under the systemId |
-
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 withunknown_target. -
The core returns
xstate.sendTowith{targetId, event, id, delay}. XState’s params also hold the receiving actor, which is not JSON; here the caller finds it fromtargetId. 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 acancel_actionof the sameidcancels it. -
A root actor has no parent: a send to
#_parentfails it withunknown_target, as XState’s root actor fails with the error its resolution throws. The pure functions return the send all the same, asxstate.sendToto#_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 (sendParentwithout a parent). -
An actor reference is not data. Where XState puts
selfin 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.logwith{value, label}, each left out when it has none. The actor layer hands each log to the callback given toon_log, with the actor, the value, a null pointer forundefined, and the label, empty when there is none: XState’sloggeroption, which receives(label, value)or(value). -
A fixed message is a
value_makerthat returns it, aslogMessagedoes for XState’slog('incremented'). -
logCountread the context afteradd: 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_logholds one callback for the whole system, which runs inside the actor’s turn as the callback ofon_actiondoes.
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_emittedhears every event the actor emits. XState also takes the type to listen to,actor.on('someStaticEvent', ...); in xstate a listener comparestype, 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_emittedreturns xactor’sinvalid_argumentfor an address that names no actor. A listener runs inside the actor’s turn, as the callback ofon_actiondoes: a call from it that changes the system,on_emittedamong them, returnsinvalid_argumenttoo and changes nothing (A callback that calls the system back). -
The core returns
xstate.emitwith{event}. The actor layer hands the event to the listeners once the macrostep has settled, as XState defers it: aftertrack, 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
emitfunction they receive. In xstate only a machine emits, withemit_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 |
|---|---|---|
|
the state |
|
|
the child actor whose id is |
|
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 |
|---|---|---|
|
|
|
|
|
|
|
the config’s |
|
|
|
|
|
the invoke’s |
|
|
|
|
a final state’s |
the final state’s |
|
the machine’s |
the root’s |
|
|
|
|
|
|
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 |
|---|---|---|
|
|
the initial snapshot and its actions |
|
|
the snapshot an event leads to and its actions |
|
|
the initial snapshot |
|
|
the snapshot an event leads to |
|
|
each microstep of the initial macrostep |
|
|
each microstep of an event’s macrostep |
|
|
whether the event selects a transition with a target or actions, as a |
|
|
every transition the snapshot could take |
|
|
the snapshot of a state value, as a |
none |
|
the nodes a state value names (The active state nodes) |
|
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 itsparamsfrom the config, null when the config gives none, andbuiltinfalse, even when the config gives it the type of a built-in action, such asxstate.raise. Executing it is the caller’s work (Custom actions). -
An
assignmerges its update into the context. It never comes back. -
Every other built-in action comes back with its params resolved and
builtinset: a raise asxstate.raise, a log asxstate.log, a cancel asxstate.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.raisewith adelayin its params, and anxstate.cancel, are for the caller’s clock (Delayed events and the simulated clock). -
xstate.spawnChild,xstate.stopChild,xstate.sendToandxstate.emitare for whatever runs the machine’s children and its listeners. The actor layer does that (Actors). -
An
xstate.logholds 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::nulloptfor the initial microstep, which reports none, for a microstep that failed while it ran, and for anext()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
microstepit 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.raisethat has a delay, and cancels on a built-inxstate.cancel. A delayed built-inxstate.sendTowith 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, andpop_due()never returns it. A custom action changes nothing, whatever its type (builtintells the two kinds apart, Actions), and neither does any other action. schedule(event, delay, id)-
holds an event for
delaymilliseconds, 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 underidhas 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.
setrefuses to move it back, withinvalid_config, where XState’ssetthrows. Neithersetnorincrementreleases 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::nulloptwhen 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, |
yes |
yes |
yes |
yes |
yes |
A host actor, |
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
startwaits, and runs after the initial macrostep, as XState’s mailbox holds it until the actor starts. A secondstartdoes nothing. -
stopstops a root actor and its whole family, each after its descendants, and returns them. For a child it is xactor’sinvalid_argumentand changes nothing: a child is its parent’s to stop, as XState’sactor.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
nullptrbefore the actor is constructed, and for a host actor. xstate constructs a root onstart, where XState’screateActorconstructs it at once, so XState’sgetSnapshot()has the initial snapshot beforestart()(The root is constructed onstart).
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
-
idis the actor’s id, whichid_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 oneactor_systemwith one root (One system peractor_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 onstart). -
system_idregisters 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 withsystem_id_taken. The empty name registers nothing, as XState registers only a truthy one. -
inputis 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)handsfeach snapshot the actor settles in while it is active, and the one it is done in: XState’sactor.subscribe. A failed actor’s snapshot is not handed over; XState hands the error to an observer’serrorcallback, which xstate does not have, andstatus_oftells it. -
on_emitted(actor, f)handsfeach event the actor emits, in order: XState’sactor.on('*', f)(Emitting events). The listener reads the event’s type. -
on_action(f)handsfevery custom action of every actor, with the actor that returned it. -
on_log(f)handsfevery log of every actor (Log), with the actor, the value,nullptrfor JavaScript’sundefined, and the label: XState’sloggeroption. 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 |
|---|---|
|
|
|
|
|
|
|
none in the inspector: |
|
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: amachine_actoror ahost_actor. XState also takes inline logic, which JSON cannot hold; a name is XState’ssetup({ actors }), the logic registered there. A name the implementations do not hold is refused when the machine is created, withunknown_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.inputscomputes 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>, withoutputandactorId, when it is done; theoutputis left out when it has none (Actor done events). -
xstate.error.actor.<id>, witherrorandactorId, 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 |
|---|---|
|
|
|
|
|
|
|
|
|
a |
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
SimulatedClockfires them. A timer armed during the tick and already due fires in the same call (A timer that falls due duringclock_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
SimulatedClocktakes fractions (Time in whole milliseconds). -
Bring the time forward only.
clock_tickdoes not refuse a time before the last one, and a timer armed after it counts from the earlier time; XState’ssetthrows for it, and the core’ssimulated_clockrefuses it withinvalid_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 |
|---|---|
|
The host actor. |
|
The actor that invoked or spawned it. |
|
Its id, as its parent knows it. |
|
The name it is registered under. |
|
Its input; none for JavaScript’s |
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_aloneanduser_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-wasip3does 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 |
wasip2 |
The toolset |
wasip3 |
The toolset |
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.mjsruns the machine cases. Each macrostep after the initial one runs through XState’smachine.transition, with an actor scope of the script’s own that records every action and every@xstate.microstepinspection event, and whose actor has a parent, so that asendParentresolves as it does in an actor that has one. XState’s pure functions cannot serve: their actor has no parent, and throws onsendParent. The initial macrostep runs throughmachine.getInitialSnapshot, with the same scope; XState’s inspection does not report its first microstep, so the snapshots and actions of its microsteps are taken fromgetInitialMicrosteps. Delayed events go to XState’s ownSimulatedClock. -
actors.mjsruns 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>intest/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_COLORset andFORCE_COLORremoved, 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 |
|---|---|
|
The error codes, state values, paths and events. Its |
|
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. |
|
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 |
|
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. |
|
The events actors send each other and the system that delivers them: a
|
|
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. |
|
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. |
|
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 |
|---|---|
|
Every machine case, one test case each, against XState’s record. |
|
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
-
A machine is created once from its config and never changes. A config’s
states,onandafterare 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 itsafterschedules. Creating a machine refuses each of these, with an error and no machine;create_machinegives 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
idorsystemIdthat is not a string; -
a compound state without
initial; -
an
initialor a target that names no state, but for a history at the root, which XState accepts and so does xstate (guarantee 23); -
an
onkey of""; -
a transition that declares a truthy
cond, renamedguardin XState v5, whose development build refuses it; -
a guard, or a named delay of an
afterkey, 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
afterkey 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
srcis not a string (An invoke’ssrc), and an actor the implementations do not hold (guarantee 22); -
an
entry,exitoractionsthat is null, which XState’stoArrayreads as a list that holds null and throws on when it runs it; -
a history’s
targetthat 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
tagsthat 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::inputsorimplementations::outputsthat names what it may not (guarantee 24).
-
-
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. -
assignchanges the context, merging the partial update its implementation returns asObject.assigndoes, and is never returned as an action.raise,log,cancel,sendParentand the actions of guarantee 21 are returned with their resolved params,sendParentas XState’sxstate.sendToto#_parent. -
A
raisewithout a numeric delay goes to the macrostep’s internal queue; one with a delay is only returned, for the caller’s clock. -
A state with
afterraisesxstate.after.<delay>.<id>, with that id and delay, on entry, and cancels it on exit, as XState does. -
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. -
progress::settledis 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 notactive. An external event that selects no transition settles on the firstnext(), 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 firstnext()fails the macrostep withinvalid_event, as XState’s development build refuses it, while an event of that type the machine raises takes the wildcard transitions. -
The core never stops a macrostep: an
alwayscycle 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. -
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
assignran), 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. -
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
orderinstead, 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. -
An implementation’s failure sets the snapshot’s
statustoerror, 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; itsprogress::resolved_before_failureholds the actions it resolved before the failure, which a running actor of XState has run in part (guarantee A9). -
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). -
simulated_clockreleases due events by (deadline, scheduling order), one at a time, and cannot be set back in time. -
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 actionxstate.raiseorxstate.cancel, which a definition cannot tell from the raise and the cancel anafteradds. 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, isinvalid_config. -
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. -
A machine starts with an input, null when none. The event
xstate.initcarries a non-null input as its memberinput. Whenimplementations::contextis set, the initial context is what it returns for the input, in place of the config’scontext; a failure, or a value that is not an object, fails the initial macrostep on its firstnext(), with the snapshot before it (XState’s_getPreInitialState). -
A state’s
invoke, one object or a list, names a child actor by its stringsrc, with an optionalid, by default<index>.<state id>(XState’screateInvokeId),inputandsystemId. ItsonDone,onErrorandonSnapshotare transitions onxstate.done.actor.<id>,xstate.error.actor.<id>andxstate.snapshot.<id>. A node’s transitions are kept in XState’sMaporder:on,onDone, each invoke’s three,after; a descriptor given twice keeps its first place and its last transitions, except underafter, whose transitions append. -
Entering a state returns, after its entry actions and before its initial actions, one
xstate.spawnChildper invoke, its params{id, src, input?, systemId?}, and adds the child to the snapshot’schildren; exiting it returns, after its exit actions, onexstate.stopChildper 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.childrenlists(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. -
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.stopChildper child left, inmacrostep_result::actionsand in no microstep; the snapshot keeps its children (XState’sstopChildren, whose snapshot XState drops). A macrostep that guarantee 20 settles, or one that fails, returns none. -
An external event whose type starts with
xstate.error.actorand that selects no transition settles in one microstep, with an empty list of transitions and no action: the snapshot the macrostep began from, its statuserror, its erroractor_failedand itserror_valuethe event’s membererror, when it has one. Its children stay, and noxstate.stopChildis returned (XState’smacrostep). -
The actions that name other actors are registered by name and returned with XState’s params:
spawn_child_actionasxstate.spawnChild{id, src, input?, systemId?}, adding the child;stop_child_actionasxstate.stopChild, as in guarantee 18;send_to_actionandforward_to_action, which sends the microstep’s event, asxstate.sendTo{targetId, event, id?, delay?};emit_actionasxstate.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’sdeferredActorIds); otherwise the microstep fails withunknown_target. A sendTo naming an invoke of the state being entered by its bare id carriesaction::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’sretryResolveSendTo); any other action has none. -
Every actor a machine names, an invoke’s
srcor thesrcof aspawn_child_actionthe config names as an action, is an entry ofimplementations::actors; creating the machine refuses any other withunknown_actor. A registeredspawn_child_actionthe config never names is not checked, as XState’ssetupaccepts it. -
A history at the root has no parent to resolve its default against. When its
targetis 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 withunknown_target, where XState’sresolveHistoryDefaultTransitionthrows. 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). -
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’sundefined: 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’sgetMachineOutputcomputes it. Creating the machine refuses, withinvalid_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 returnsout_of_fuelwhen an actor is left parked,settledotherwise. While an actor is parked,start,send,clock_tick,resolveandrejectare checked and then held, in the order made, and returnout_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.stopis 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
createActorandstart(): 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’sstart()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’sstart. 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
resolveActionsAndContextdoes, then the stopChild actions the macrostep returns after its microsteps (guarantee 19):-
a custom action reaches
on_action, andxstate.logon_log; -
xstate.spawnChildclaims its systemId, if any, and creates the childimplementations::actorsnames 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’screateActorconstructs a child insideresolveSpawn. Its start is deferred; -
xstate.stopChildreleases 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 onstart(); -
xstate.sendToresolves 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 withunknown_targetthere, as XState’s development build throws). One that carriesbound_at(guarantee 21) is bound to the child its id names once that many of the microstep’s actions are resolved, as XState’sretryResolveSendTobinds 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.cancelandxstate.emitare 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_stopProceduredoes 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’serror_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
timerMapnames the latest.clock_tick(now)releases the timers due bynowone 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’sSimulatedClockfires them; one armed meanwhile and due bynowis 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
systemIdnames 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):getfinds 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 withsystem_id_takenif a child the parent’s macrostep created, or one descended from it, holds the name then, as XState’sstart()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
errorand its errorsystem_id_takenorunknown_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 onstart(). 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;resolveandrejectend it, done or failed, and its parent receives its done or error event. A request whose actor has ended is no longer pending, andresolveorrejectof it isinvalid_argumentand 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.
stopstops a root actor and its family from the host, at once; for a child it isinvalid_argumentand changes nothing, as XState’sactor.stop()refuses a child. - A12
-
The host’s callbacks, a
subscribeoron_emittedlistener, theon_actionandon_logcallbacks and the inspector, run inside an actor’s turn. A call from one of them that changes the system isinvalid_argumentand changes nothing; theconstones, which read it, are free.subscribeandon_emittedadd a listener of one actor, called in the order added, and refuse an address that names no actor withinvalid_argument, asstartandsenddo;on_action,on_logandinspecthold 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 |
|---|---|
Defined when xstate is built without exceptions: when webcpp has none (the compiler has none, or the developer defined |
webcpp::xstate
Types
| Name | Description |
|---|---|
An action returned to the caller to execute. |
|
What an implementation is handed, built by xstate for the call. |
|
An action as the config names it, or one of the two XState adds to a state with |
|
How an actor the host creates starts, as XState’s |
|
What the layer keeps of one actor. |
|
A system of actors on one xactor scheduler, run by its host. |
|
An assign, which merges a partial update into the context. |
|
A cancel of a delayed raise or send. |
|
Constructs a machine child: it runs its initial macrostep and resolves its actions, then answers |
|
A child’s answer to |
|
A delayed event to the actor itself, armed under an id. |
|
An event emitted to the listeners of the actor, not to a machine. |
|
An event: its type, and the members that travel with it. |
|
A send of the microstep’s event to another actor. |
|
A guard as the config names it: a registered guard, by name, with its params, or one of XState’s higher-order guards. |
|
An actor whose work is done outside the machines, by the host. |
|
The host’s answer to a host actor: its output, or its error. |
|
The logic of a host actor, in place of XState’s |
|
A host actor’s request, pending until the host resolves or rejects it. |
|
The implementations a machine is created with, each registered by the name its config gives it. |
|
What an inspector hears, after XState’s |
|
A child actor a state invokes, which lives while the state is active. |
|
A log of a value, with an optional label. |
|
The immutable tree of state nodes |
|
A machine run as a child actor, with that machine’s own implementations. |
|
The logic of a machine actor, XState’s |
|
The cursor of one macrostep, which runs one microstep a call. |
|
A settled macrostep: its stable snapshot, every action its microsteps returned, and how many microsteps ran. |
|
One microstep: the event it ran for, the transitions it took, the actions it returned and the snapshot it left. |
|
A set of node indices that keeps the order they were added in. |
|
What one call of |
|
A raise, which sends an event to the machine itself. |
|
A delayed sendTo whose timer fired: its sender forwards |
|
Continues an actor the fuel of an earlier execution could not finish. |
|
A send to the machine’s parent actor. |
|
A send of an event to another actor, named by a target string. |
|
A clock the caller owns, which holds the delayed events of the machine core until the time it is set to reaches them. |
|
Where a machine is, as a plain value to keep, copy or store. |
|
A spawn of a child actor that lives until it is stopped or the machine is done. |
|
Starts the actor, constructing it first if no parent did. |
|
One node of a machine. |
|
Asks an actor to stop with its family, once it has handled what its parent sent it before. |
|
A stop of a child actor. |
|
What an actor system knows beside xactor’s statuses, shared by every actor of the system. |
|
One transition of a node. |
Type Aliases
| Name | Description |
|---|---|
The built-in action an entry of |
|
What an invoke’s or a spawnChild’s |
|
Every message an actor of the layer receives. |
|
Returns the partial update an assign merges into the context. |
|
Returns the initial context from the input the machine was started with. |
|
Returns a delay, in milliseconds. |
|
A delay as an action names it: a number of milliseconds, or the name of a delay of the registry. |
|
Returns the event a raise, a sendTo, a sendParent or an emit sends. |
|
Computes a spawned child’s id. |
|
Decides whether a transition is taken. |
|
What a function that can fail returns: a |
|
Returns a value, or none for JavaScript’s |
Enums
| Name | Description |
|---|---|
The errors xstate reports, as |
|
A history node’s kind, from its |
|
A node’s type, as XState’s |
|
What a host call left: every actor settled, or one parked for want of fuel. |
|
A snapshot’s status. |
Functions
| Name | Description |
|---|---|
Makes the cursor of the macrostep an event runs from a snapshot. |
|
Makes the cursor of a machine’s initial macrostep. |
|
Whether an event would take, from a snapshot, a transition that does something. |
|
Builds a machine from the JSON of XState’s |
|
Loads a machine back from a definition, with the implementations and the context, which a definition does not hold. |
|
The event an actor that ends sends its parent. |
|
The category of every |
|
Reads an event from XState’s flat JSON form. |
|
Builds a failed result of the type the call would have returned. |
|
Runs a machine’s initial macrostep and returns every microstep. |
|
Runs a machine’s initial macrostep and returns the snapshot it starts in. |
|
The |
|
Runs the macrostep of an event and returns every microstep. |
|
Runs the macrostep of an event and returns the snapshot it leads to. |
|
Lists every transition a snapshot could take, guards not evaluated. |
|
Runs a machine’s initial macrostep and returns its snapshot and its actions. |
|
Makes the |
|
Whether the state value |
|
Builds the state value a path describes. |
|
Builds the snapshot of a state value, completed to every node it implies, with a context. |
|
|
|
Splits a state id into the keys of its path. |
|
Converts a state value given as a dotted string to its object form. |
|
Runs the macrostep of an event and returns the snapshot it leads to and its actions. |
Variables
| Name | Description |
|---|---|
The type of the event every initial macrostep runs with, "xstate.init". |
|
The descriptor that matches every event, "*". |
webcpp::xstate::action_implementation
The built-in action an entry of implementations::actions stands for.
Synopsis
Declared in <webcpp/xstate/implementations.hpp>
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
Declared in <webcpp/xstate/implementations.hpp>
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
Declared in <webcpp/xstate/actors/message.hpp>
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
Declared in <webcpp/xstate/implementations.hpp>
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
Declared in <webcpp/xstate/implementations.hpp>
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
Declared in <webcpp/xstate/implementations.hpp>
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
Declared in <webcpp/xstate/implementations.hpp>
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
Declared in <webcpp/xstate/implementations.hpp>
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
Declared in <webcpp/xstate/implementations.hpp>
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
Declared in <webcpp/xstate/implementations.hpp>
using predicate = std::function<result<bool>(action_args const&)>;
Description
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; |
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
Declared in <webcpp/xstate/implementations.hpp>
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.
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 araise_actionand on entering a state withafter, its paramsevent,idwhen it has one anddelaywhen delayed; -
xstate.cancel, for acancel_actionand on leaving a state withafter, its paramssendId; -
xstate.log, for alog_action, its paramsvalueunless undefined andlabelwhen it has one; -
xstate.sendTo, for asend_to_action, asend_parent_actionand aforward_to_action, its paramstargetId,event,idwhen it has one anddelaywhen delayed; -
xstate.emit, for anemit_action, its paramsevent; -
xstate.spawnChild, for aspawn_child_actionand on entering a state that invokes, its paramsid,src, andinputandsystemIdwhen there are any; -
xstate.stopChild, for astop_child_action, on leaving a state that invokes, and for each child left when a macrostep settles not active, its paramsid, or null when the child was not there.
An assign is never returned.
|
Note
|
|
Data Members
| Name | Description |
|---|---|
Where XState binds a sendTo that names, by its bare id, an invoke of the state being entered. |
|
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. |
|
Whether the action is a forwardTo’s sendTo. |
|
The resolved params of a built-in, or the params the config gave, null when none. |
|
The action’s type: the name the config gave, or a built-in’s |
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.
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.
webcpp::xstate::action::forwarded
Whether the action is a forwardTo’s sendTo.
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.
webcpp::xstate::action::type
The action’s type: the name the config gave, or a built-in’s xstate. name.
webcpp::xstate::action_args
What an implementation is handed, built by xstate for the call.
Description
Every implementation but implementations::context is handed one. The references live during the call.
Data Members
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.
webcpp::xstate::action_args::event
The microstep’s 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.
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.
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 |
|---|---|
Where an action comes from: the config, or an |
Data Members
| Name | Description |
|---|---|
The action as the config wrote it, for the definition. |
|
The delay of an after_raise. |
|
Where this action comes from. |
|
The action’s params, null when the config gave none. |
|
The action’s name, or, for the two |
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.
Members
| Name | Description |
|---|---|
An action the config names. |
|
The delayed raise an |
|
The cancel an |
webcpp::xstate::action_ref::kind::named
An action the config names.
webcpp::xstate::action_ref::kind::after_raise
The delayed raise an after adds to its state’s entry.
webcpp::xstate::action_ref::kind::after_cancel
The cancel an after adds to its state’s exit.
webcpp::xstate::action_ref::config
The action as the config wrote it, for the definition.
webcpp::xstate::action_ref::delay
The delay of an after_raise.
webcpp::xstate::action_ref::kind
Where this action comes from.
webcpp::xstate::action_ref::params
The action’s params, null when the config gave none.
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.
webcpp::xstate::actor_options
How an actor the host creates starts, as XState’s createActor options say.
Description
actor_system::create_actor takes it.
Data Members
| Name | Description |
|---|---|
The actor’s id, which |
|
The actor’s input, null for none. |
|
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.
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.
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
Declared in <webcpp/xstate/actors/actor_system.hpp>
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.
Description
system_state holds one for each actor, from its creation on.
Data Members
| Name | Description |
|---|---|
The children it spawned and still has, by id, in spawning order. |
|
A machine actor’s snapshot, once its first macrostep has settled, which |
|
The id its parent knows it by, an invoke’s or a spawnChild’s id, or the host’s id for a root. |
|
The actor that spawned it; none for an actor the host created. |
|
The implementation it runs, by its name in |
|
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
Declared in <webcpp/xstate/actors/system_state.hpp>
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.
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.
webcpp::xstate::actor_record::parent
The actor that spawned it; none for an actor the host created.
Synopsis
Declared in <webcpp/xstate/actors/system_state.hpp>
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.
webcpp::xstate::actor_record::system_id
The systemId it claimed when it was created, if any, which it may have released since.
Synopsis
Declared in <webcpp/xstate/actors/system_state.hpp>
std::optional<std::string> system_id = {};
webcpp::xstate::actor_system
A system of actors on one xactor scheduler, run by its host.
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 |
|---|---|
|
Constructors |
|
Destroys the system and every actor in it. |
Assignment operators |
|
The child an actor knows by an id, while it has it. |
|
Moves the system’s time to |
|
Creates an actor that runs a machine, not yet started. |
|
The actor registered under a systemId, while it holds it. |
|
The pending requests of the host actors, in the order their actors started. |
|
The id an actor’s parent knows it by, or the host’s id for it. |
|
Hands the system’s inspector what every actor does. |
|
Hands every custom action of every actor to a listener, with the actor that returned it. |
|
Adds a listener of the events an actor emits. |
|
Hands every |
|
The actor that spawned an actor. |
|
Answers a pending request with an error, and runs the system. |
|
Answers a pending request with an output, and runs the system. |
|
Gives every parked actor a new execution and runs, then applies the host’s calls held while an actor was parked. |
|
Runs the system until no message is left. |
|
Sends an actor an event from the host, and runs the system. |
|
A machine actor’s snapshot, once it has been constructed. |
|
Starts an actor, constructing it first, and runs the system. |
|
Where an actor is in its life, as its scheduler keeps it. |
|
Stops a root actor and its whole family at once. |
|
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
Declared in <webcpp/xstate/actors/actor_system.hpp>
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
Declared in <webcpp/xstate/actors/actor_system.hpp>
actor_system(actor_system const&) = delete;
webcpp::xstate::actor_system::actor_system
Not movable: every actor holds a reference to the system’s state.
webcpp::xstate::actor_system::actor_system
Makes a system with no actor, whose every execution starts with limits.fuel.
Synopsis
Declared in <webcpp/xstate/actors/actor_system.hpp>
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.
webcpp::xstate::actor_system::operator=
Assignment operators
Synopses
Declared in <webcpp/xstate/actors/actor_system.hpp>
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.
actor_system&
operator=(actor_system&&) = delete;
webcpp::xstate::actor_system::operator=
Not copy-assignable: every actor holds a reference to the system’s state.
Synopsis
Declared in <webcpp/xstate/actors/actor_system.hpp>
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
Declared in <webcpp/xstate/actors/actor_system.hpp>
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
Declared in <webcpp/xstate/actors/actor_system.hpp>
[[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
Declared in <webcpp/xstate/actors/actor_system.hpp>
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 |
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
Declared in <webcpp/xstate/actors/actor_system.hpp>
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
Declared in <webcpp/xstate/actors/actor_system.hpp>
[[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
Declared in <webcpp/xstate/actors/actor_system.hpp>
[[nodiscard]]
std::vector<host_request>
host_requests() const;
Description
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
Declared in <webcpp/xstate/actors/actor_system.hpp>
[[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.
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.
Synopsis
Declared in <webcpp/xstate/actors/actor_system.hpp>
result<void>
on_action(system_state::action_listener listener);
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
Declared in <webcpp/xstate/actors/actor_system.hpp>
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.
Synopsis
Declared in <webcpp/xstate/actors/actor_system.hpp>
result<void>
on_log(system_state::log_listener listener);
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
Declared in <webcpp/xstate/actors/actor_system.hpp>
[[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
Declared in <webcpp/xstate/actors/actor_system.hpp>
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 |
error |
The error, none for JavaScript’s |
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
Declared in <webcpp/xstate/actors/actor_system.hpp>
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 |
output |
The output, none for JavaScript’s |
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.
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
Declared in <webcpp/xstate/actors/actor_system.hpp>
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
Declared in <webcpp/xstate/actors/actor_system.hpp>
[[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
Declared in <webcpp/xstate/actors/actor_system.hpp>
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 |
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
Declared in <webcpp/xstate/actors/actor_system.hpp>
[[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
Declared in <webcpp/xstate/actors/actor_system.hpp>
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
Declared in <webcpp/xstate/actors/actor_system.hpp>
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.
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 |
|---|---|
Computes the partial update. |
See Also
"Assign", in the guide.
webcpp::xstate::assign_action::assignment
Computes the partial update.
webcpp::xstate::cancel_action
A cancel of a delayed raise or send.
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 |
|---|---|
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.
webcpp::xstate::construct_actor
Constructs a machine child: it runs its initial macrostep and resolves its actions, then answers constructed.
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.
webcpp::xstate::delayed
A delayed event to the actor itself, armed under an id.
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
See Also
"Guarantees", in the guide: guarantee A7.
webcpp::xstate::emit_action
An event emitted to the listeners of the actor, not to a machine.
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 |
|---|---|
Computes the event to emit. |
See Also
"Emitting events", in the guide.
webcpp::xstate::emit_action::event
Computes the event to emit.
webcpp::xstate::event
An event: its type, and the members that travel with it.
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
See Also
"Events", in the guide.
webcpp::xstate::forward_to_action
A send of the microstep’s event to another actor.
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
See Also
"Sending to other actors", in the guide.
webcpp::xstate::forward_to_action::delay
The delay, none for a send at once.
webcpp::xstate::forward_to_action::id
The id a cancel names a delayed send by.
webcpp::xstate::forward_to_action::target
The actor to forward to, as the target string names it.
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.
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 ( |
Enums
| Name | Description |
|---|---|
What a guard is: a registered one, or one of the higher-order guards. |
Data Members
| Name | Description |
|---|---|
The guard as the config wrote it, for the definition. |
|
What this guard is. |
|
A higher-order guard’s operands, by their index in |
|
A named guard’s params, null when the config gave none. |
|
A stateIn guard’s state value, or its |
|
The guard’s type: its name, or |
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.
Members
| Name | Description |
|---|---|
A registered guard, by name. |
|
|
|
|
|
|
|
|
webcpp::xstate::guard_node::kind::named
A registered guard, by name.
webcpp::xstate::guard_node::kind::negation
xstate.not, which holds when its one operand does not.
webcpp::xstate::guard_node::kind::conjunction
xstate.and, which holds when every operand does.
webcpp::xstate::guard_node::kind::disjunction
xstate.or, which holds when at least one operand does.
webcpp::xstate::guard_node::kind::state_in
xstate.stateIn, which holds when the snapshot is in a state.
webcpp::xstate::guard_node::config
The guard as the config wrote it, for the definition.
webcpp::xstate::guard_node::kind
What this guard is.
webcpp::xstate::guard_node::operands
A higher-order guard’s operands, by their index in machine::guard.
webcpp::xstate::guard_node::params
A named guard’s params, null when the config gave none.
webcpp::xstate::guard_node::state_value
A stateIn guard’s state value, or its #id.
webcpp::xstate::guard_node::type
The guard’s type: its name, or xstate.not and the others.
webcpp::xstate::host_actor
An actor whose work is done outside the machines, by the host.
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.
Description
actor_system::resolve and actor_system::reject send it.
Data Members
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.
webcpp::xstate::host_answer::value
The output or the error; none for a promise settled with JavaScript’s undefined.
Synopsis
Declared in <webcpp/xstate/actors/message.hpp>
std::optional<boost::json::value> value = {};
webcpp::xstate::host_logic
The logic of a host actor, in place of XState’s fromPromise.
Synopsis
Declared in <webcpp/xstate/actors/host_logic.hpp>
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 |
|---|---|
|
Type Aliases
| Name | Description |
|---|---|
A message as the scheduler delivers it: a |
|
What the actor may do while it handles a message: a |
Member Functions
| Name | Description |
|---|---|
|
Makes the logic of a host actor. |
|
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
Declared in <webcpp/xstate/actors/host_logic.hpp>
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
Declared in <webcpp/xstate/actors/host_logic.hpp>
using turn_type = xactor::turn<actor_message>;
webcpp::xstate::host_logic::host_logic
Makes the logic of a host actor.
Synopsis
Declared in <webcpp/xstate/actors/host_logic.hpp>
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 |
webcpp::xstate::host_logic::handle
Handles one message of the actor’s mailbox.
Synopsis
Declared in <webcpp/xstate/actors/host_logic.hpp>
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.
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 |
|---|---|
The host actor, which the answer ends. |
|
The host actor’s id, its invoke’s or its spawnChild’s. |
|
The host actor’s input; none when it was given none, XState’s |
|
The actor that invoked or spawned the host actor. |
|
The name of the host actor in |
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.
webcpp::xstate::host_request::id
The host actor’s id, its invoke’s or its spawnChild’s.
webcpp::xstate::host_request::input
The host actor’s input; none when it was given none, XState’s undefined.
Synopsis
Declared in <webcpp/xstate/actors/system_state.hpp>
std::optional<boost::json::value> input = {};
webcpp::xstate::host_request::parent
The actor that invoked or spawned the host actor.
webcpp::xstate::host_request::src
The name of the host actor in implementations::actors, its invoke’s or its spawnChild’s src.
webcpp::xstate::implementations
The implementations a machine is created with, each registered by the name its config gives it.
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 |
|---|---|
The built-in actions, by action name; any other name is a custom action. |
|
What an invoke or a spawnChild runs, by its |
|
Computes the initial context from the machine’s input, in place of the config’s |
|
The delays an |
|
The predicates a transition’s |
|
The input an invoke’s child is given, by invoke id, in place of the config’s |
|
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 |
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
Declared in <webcpp/xstate/implementations.hpp>
std::map<std::string, action_implementation, std::less<>> actions = {};
webcpp::xstate::implementations::actors
What an invoke or a spawnChild runs, by its src.
Synopsis
Declared in <webcpp/xstate/implementations.hpp>
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.
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
Declared in <webcpp/xstate/implementations.hpp>
std::map<std::string, delay_maker, std::less<>> delays = {};
webcpp::xstate::implementations::guards
The predicates a transition’s guard names, by guard name.
Synopsis
Declared in <webcpp/xstate/implementations.hpp>
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
Declared in <webcpp/xstate/implementations.hpp>
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
Declared in <webcpp/xstate/implementations.hpp>
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.
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 |
Data Members
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
Declared in <webcpp/xstate/actors/system_state.hpp>
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
Declared in <webcpp/xstate/actors/system_state.hpp>
webcpp::xstate::inspector::started
Hears an actor’s start, with the actor’s address.
Synopsis
Declared in <webcpp/xstate/actors/system_state.hpp>
std::function<void(xactor::actor_ref)> started = {};
webcpp::xstate::invoke_definition
A child actor a state invokes, which lives while the state is active.
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 |
Data Members
| Name | Description |
|---|---|
The invoke as the config wrote it, for its transitions and the definition. |
|
The config’s id, or XState’s default |
|
The config’s |
|
The name of an entry of |
|
The config’s |
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.
webcpp::xstate::invoke_definition::id
The config’s id, or XState’s default <index>.<state id>.
webcpp::xstate::invoke_definition::input
The config’s input, absent when it has none, XState’s undefined.
webcpp::xstate::invoke_definition::src
The name of an entry of implementations::actors.
webcpp::xstate::invoke_definition::system_id
The config’s systemId.
webcpp::xstate::log_action
A log of a value, with an optional label.
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 |
Data Members
See Also
"Log", in the guide.
webcpp::xstate::log_action::label
The label, written with the value.
webcpp::xstate::log_action::value
Computes the value to log; without one, the context and the event.
webcpp::xstate::machine
The immutable tree of state nodes create_machine builds from a config, with the implementations it was given.
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 |
|---|---|
Finds the child of a node by its key. |
|
The config the machine was created from. |
|
The initial context, unless |
|
The guard at an index of the machine’s flat list of guards. |
|
The machine’s id. |
|
The node at an index. |
|
Finds the node a state id names. |
|
Finds the node a dotted path names, walking down from a node. |
|
The implementations the machine was created with. |
|
The root node. |
|
How many nodes the machine has. |
Friends
| Name | Description |
|---|---|
Builds a machine from the JSON of XState’s |
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
Declared in <webcpp/xstate/machine.hpp>
[[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
Declared in <webcpp/xstate/machine.hpp>
[[nodiscard]]
boost::json::object const&
config() const noexcept;
webcpp::xstate::machine::context
The initial context, unless implementations::context computes it.
Synopsis
Declared in <webcpp/xstate/machine.hpp>
[[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.
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
Declared in <webcpp/xstate/machine.hpp>
[[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.
Parameters
| Name | Description |
|---|---|
index |
The guard’s index. |
Preconditions
-
indexis the index of a transition’s guard or of an operand.
webcpp::xstate::machine::id
The machine’s id.
Synopsis
Declared in <webcpp/xstate/machine.hpp>
[[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
Declared in <webcpp/xstate/machine.hpp>
[[nodiscard]]
state_node const&
node(std::size_t index) const;
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
Declared in <webcpp/xstate/machine.hpp>
[[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
Declared in <webcpp/xstate/machine.hpp>
[[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 |
webcpp::xstate::machine::registry
The implementations the machine was created with.
Synopsis
Declared in <webcpp/xstate/machine.hpp>
[[nodiscard]]
implementations const&
registry() const noexcept;
webcpp::xstate::machine_actor
A machine run as a child actor, with that machine’s own implementations.
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 |
|---|---|
|
Makes the actor of a machine. |
Data Members
| Name | Description |
|---|---|
The machine the child runs. |
See Also
"Invoking actors", in the guide.
webcpp::xstate::machine_actor::machine_actor
Makes the actor of a machine.
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.
webcpp::xstate::machine_logic
The logic of a machine actor, XState’s createActor for a machine.
Synopsis
Declared in <webcpp/xstate/actors/machine_logic.hpp>
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 |
|---|---|
|
Type Aliases
| Name | Description |
|---|---|
A message as the scheduler delivers it: a |
|
What the actor may do while it handles a message: a |
Member Functions
| Name | Description |
|---|---|
|
Makes the logic of a machine actor, which runs nothing until it is constructed or started. |
|
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
Declared in <webcpp/xstate/actors/machine_logic.hpp>
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
Declared in <webcpp/xstate/actors/machine_logic.hpp>
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
Declared in <webcpp/xstate/actors/machine_logic.hpp>
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
Declared in <webcpp/xstate/actors/machine_logic.hpp>
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.
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 |
|---|---|
The snapshot the macrostep began from. |
|
The snapshot as the last microstep left it. |
|
Whether the macrostep has settled. |
|
Runs exactly one microstep. |
|
The settled macrostep. |
Friends
| Name | Description |
|---|---|
Makes the cursor of the macrostep an event runs from a snapshot. |
|
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
Declared in <webcpp/xstate/macrostep.hpp>
[[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
Declared in <webcpp/xstate/macrostep.hpp>
[[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.
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.
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_targetwhen a sendTo names no actor, or a microstep needs the default of a history state that is the machine’s root; -
errc::implementation_failedwhen anassignruns on a context that is not an object, for example the context of a config whosecontextis5; -
errc::invalid_eventwhen the external event’s type is*; -errc::unknown_statewhen the snapshot’s value names no state of the machine, or astateInguard’s#idnames none; -
the context function’s error, or
errc::implementation_failedwhen 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
Declared in <webcpp/xstate/macrostep.hpp>
[[nodiscard]]
macrostep_result const&
result() const noexcept;
webcpp::xstate::macrostep_result
A settled macrostep: its stable snapshot, every action its microsteps returned, and how many microsteps ran.
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 |
|---|---|
Every action the microsteps returned, in order. |
|
How many microsteps ran, a failing one excluded. |
|
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.
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.
webcpp::xstate::macrostep_result::snapshot
The stable snapshot the macrostep settled in.
webcpp::xstate::microstep
One microstep: the event it ran for, the transitions it took, the actions it returned and the snapshot it left.
Description
It is what XState’s getMicrosteps and getInitialMicrosteps list for each microstep.
Data Members
| Name | Description |
|---|---|
The actions the microstep returned, in order. |
|
The event the microstep ran for: the macrostep’s, a raised one, or the last one again for an eventless transition. |
|
The snapshot the microstep left. |
|
The transitions the microstep took, as pointers into the machine. |
See Also
"Microsteps", in the guide.
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.
webcpp::xstate::microstep::transitions
The transitions the microstep took, as pointers into the machine.
Synopsis
Declared in <webcpp/xstate/macrostep.hpp>
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.
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 |
|---|---|
|
Constructors |
The node at a position of the insertion order. |
|
An iterator to the first node, in insertion order. |
|
Whether a node is in the set. |
|
Whether the set holds no node. |
|
The end of the nodes, in insertion order. |
|
Removes a node, the others keeping their order. |
|
Adds a node at the end, unless it is already in the set. |
|
The nodes, in insertion order. |
|
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
Declared in <webcpp/xstate/node_set.hpp>
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 |
Parameters
| Name | Description |
|---|---|
nodes |
The nodes, each inserted as |
webcpp::xstate::node_set::node_set
Makes the set of the nodes of a range, in the range’s order.
Synopsis
Declared in <webcpp/xstate/node_set.hpp>
template<class Range>
explicit
node_set(Range const& nodes);
Template Parameters
| Name | Description |
|---|---|
Range |
A range of |
Parameters
| Name | Description |
|---|---|
nodes |
The nodes, each inserted as |
webcpp::xstate::node_set::at
The node at a position of the insertion order.
Synopsis
Declared in <webcpp/xstate/node_set.hpp>
[[nodiscard]]
std::size_t
at(std::size_t index) const;
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.
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
Declared in <webcpp/xstate/node_set.hpp>
[[nodiscard]]
bool
contains(std::size_t node) const noexcept;
Parameters
| Name | Description |
|---|---|
node |
The node’s index in its machine. |
webcpp::xstate::node_set::erase
Removes a node, the others keeping their order.
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.
Parameters
| Name | Description |
|---|---|
node |
The node’s index in its machine. |
webcpp::xstate::node_set::ordered
The nodes, in insertion order.
Synopsis
Declared in <webcpp/xstate/node_set.hpp>
[[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::progress
What one call of macrostep::next did: its microstep, and whether it settled the macrostep.
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 |
|---|---|
The actions a microstep that failed resolved before the failure. |
|
Whether the microstep settled the macrostep. |
|
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
Declared in <webcpp/xstate/macrostep.hpp>
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.
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::raise_action
A raise, which sends an event to the machine itself.
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
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.
webcpp::xstate::raise_action::event
Computes the event to raise.
webcpp::xstate::raise_action::id
The id a cancel names a delayed raise by.
webcpp::xstate::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
See Also
"Guarantees", in the guide: guarantee A7.
webcpp::xstate::relay::send_id
The id it was armed under, which its firing unregisters; none when it had none.
webcpp::xstate::resume
Continues an actor the fuel of an earlier execution could not finish.
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.
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
See Also
"Sending to other actors", in the guide.
webcpp::xstate::send_parent_action::delay
The delay, none for a send at once.
webcpp::xstate::send_parent_action::event
Computes the event to send.
webcpp::xstate::send_parent_action::id
The id a cancel names a delayed send by.
webcpp::xstate::send_to_action
A send of an event to another actor, named by a target string.
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, |
Data Members
See Also
"Sending to other actors", in the guide.
webcpp::xstate::send_to_action::delay
The delay, none for a send at once.
webcpp::xstate::send_to_action::event
Computes the event to send.
webcpp::xstate::send_to_action::id
The id a cancel names a delayed send by.
webcpp::xstate::send_to_action::target
The actor to send to, as the target string names it.
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.
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 |
|---|---|
Applies to the clock a built-in action a step returned. |
|
Forgets the event scheduled under an id, if the id still names one. |
|
Moves the time forward. |
|
The time. |
|
Takes the earliest event whose time has come out of the clock. |
|
Holds an event until a delay from now. |
|
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.
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 |
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.
Description
It ports XState’s scheduler.cancel.
Parameters
| Name | Description |
|---|---|
id |
The id. |
webcpp::xstate::simulated_clock::increment
Moves the time forward.
Description
It ports XState’s SimulatedClock.increment.
Parameters
| Name | Description |
|---|---|
milliseconds |
How far. |
webcpp::xstate::simulated_clock::now
The time.
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.
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.
webcpp::xstate::simulated_clock::set
Sets the time, which never goes back.
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.
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 |
|---|---|
Whether an active node carries a tag. |
|
Whether the snapshot is in a state value. |
|
What a history node remembers. |
Data Members
| Name | Description |
|---|---|
The child actors, |
|
The context. |
|
Why the status is |
|
With the error |
|
What each history node remembers, by its id, in recording order. |
|
The active nodes, by index, in XState’s order. |
|
The machine’s output once it is done; absent when it has none, JavaScript’s |
|
Whether the machine runs, is done, or failed. |
|
The tags of the active nodes, each once, in the order of |
|
XState’s state value, computed from |
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
Declared in <webcpp/xstate/snapshot.hpp>
[[nodiscard]]
bool
has_tag(std::string_view tag) const;
Description
It ports XState’s snapshot.hasTag.
Parameters
| Name | Description |
|---|---|
tag |
The tag. |
webcpp::xstate::snapshot::matches
Whether the snapshot is in a state value.
Synopsis
Declared in <webcpp/xstate/snapshot.hpp>
[[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
Declared in <webcpp/xstate/snapshot.hpp>
[[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
Declared in <webcpp/xstate/snapshot.hpp>
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::error
Why the status is status::error; an empty code otherwise.
webcpp::xstate::snapshot::error_value
With the error errc::actor_failed, the failed actor’s error, when its error event carried one.
Synopsis
Declared in <webcpp/xstate/snapshot.hpp>
std::optional<boost::json::value> error_value = {};
webcpp::xstate::snapshot::history
What each history node remembers, by its id, in recording order.
Synopsis
Declared in <webcpp/xstate/snapshot.hpp>
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.
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.
webcpp::xstate::snapshot::value
XState’s state value, computed from nodes.
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.
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
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
Declared in <webcpp/xstate/implementations.hpp>
std::variant<std::string, id_maker> id = {};
webcpp::xstate::spawn_child_action::input
Computes the child’s input; without one the input is undefined.
webcpp::xstate::spawn_child_action::src
The name of an entry of implementations::actors.
webcpp::xstate::spawn_child_action::system_id
The systemId the child is registered under in its actor system.
webcpp::xstate::start_actor
Starts the actor, constructing it first if no parent did.
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.
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 |
|---|---|
Its eventless transitions. |
|
Its config, without its |
|
The config’s |
|
Its entry actions, with the delayed raises an |
|
Its exit actions, with the cancels an |
|
For a history node, its kind. |
|
A history node’s default target, resolved. |
|
Its id: the config’s |
|
Its initial transition, which every node has. |
|
The children it invokes. |
|
Its key in its parent’s |
|
The config’s |
|
XState’s |
|
The config’s |
|
Its parent, none for the root. |
|
The keys from the root down to it, the root’s excluded. |
|
Its children, by key, in the order JavaScript enumerates the config’s keys. |
|
The config’s |
|
Its transitions by event descriptor, in XState’s Map order. |
|
Its type. |
See Also
"State nodes and ids", in the guide.
webcpp::xstate::state_node::always
Its eventless transitions.
webcpp::xstate::state_node::config
Its config, without its states.
webcpp::xstate::state_node::description
The config’s 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.
webcpp::xstate::state_node::exit
Its exit actions, with the cancels an after adds.
webcpp::xstate::state_node::history
For a history node, its kind.
webcpp::xstate::state_node::history_target
A history node’s default target, resolved.
Synopsis
Declared in <webcpp/xstate/state_node.hpp>
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.
webcpp::xstate::state_node::initial
Its initial transition, which every node has.
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.
webcpp::xstate::state_node::key
Its key in its parent’s states, the root’s being the machine’s id.
webcpp::xstate::state_node::meta
The config’s 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.
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.
webcpp::xstate::state_node::parent
Its parent, none for the root.
webcpp::xstate::state_node::path
The keys from the root down to it, the root’s excluded.
webcpp::xstate::state_node::states
Its children, by key, in the order JavaScript enumerates the config’s keys.
Synopsis
Declared in <webcpp/xstate/state_node.hpp>
std::vector<std::pair<std::string, std::size_t>> states = {};
webcpp::xstate::state_node::tags
The config’s tags.
webcpp::xstate::state_node::transitions
Its transitions by event descriptor, in XState’s Map order.
Synopsis
Declared in <webcpp/xstate/state_node.hpp>
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.
webcpp::xstate::stop_actor
Asks an actor to stop with its family, once it has handled what its parent sent it before.
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.
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 |
|---|---|
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.
webcpp::xstate::system_state
What an actor system knows beside xactor’s statuses, shared by every actor of the system.
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 |
|---|---|
The listener of every custom action, with the actor that returned it, which |
|
A listener of an actor’s emitted events, which |
|
The listener of every log, with the actor that returned it, which |
|
A listener of an actor’s snapshots, which |
Member Functions
| Name | Description |
|---|---|
Hands a custom action to the host’s action listener. |
|
Records an actor, the host’s or a spawned one, and its systemId. |
|
Records a child spawned by |
|
Removes the pending request of a host actor the host has answered. |
|
Adds a host actor’s pending request. |
|
Counts an actor created, or one whose creation XState begins and a claim of its systemId then refuses; XState’s system._bookId. |
|
How many actors the system created or began to create. |
|
The child an actor knows by an id, while it has it. |
|
Hands an emitted event to the inspector, then to the actor’s listeners, in listening order. |
|
An actor and every actor descended from it. |
|
Removes the child an actor knows by an id from the actor’s children. |
|
Sets the inspector, replacing the previous one. |
|
Keeps a listener of an actor’s emitted events; an empty one is none. |
|
Hands a log to the host’s log listener. |
|
Hands a settled snapshot to the actor’s subscribers, in subscribing order. |
|
Sets the listener of every custom action, replacing the previous one. |
|
Sets the listener of every log, replacing the previous one. |
|
Counts an actor among the parked, which wait for a resume. |
|
The parked actors. |
|
Keeps the snapshot a machine actor settled in, which |
|
Gives a parent back a child under an id, unless another child holds that id. |
|
What the system keeps of an actor. |
|
The actor registered under a systemId, while it holds it. |
|
Lets go of what an actor that has ended held: its systemId, its pending request and its place among the parked. |
|
The pending requests of the host actors. |
|
Tells the inspector a machine actor settled a macrostep. |
|
Tells the inspector an actor started. |
|
Keeps a listener of an actor’s snapshots; an empty one is none, as an empty callback is. |
|
Takes an actor out of the parked. |
|
Releases the systemId an actor holds, and only its; XState’s |
|
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
Declared in <webcpp/xstate/actors/system_state.hpp>
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
Declared in <webcpp/xstate/actors/system_state.hpp>
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
Declared in <webcpp/xstate/actors/system_state.hpp>
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
Declared in <webcpp/xstate/actors/system_state.hpp>
using snapshot_listener = std::function<void(snapshot const&)>;
webcpp::xstate::system_state::act
Hands a custom action to the host’s action listener.
Synopsis
Declared in <webcpp/xstate/actors/system_state.hpp>
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
Declared in <webcpp/xstate/actors/system_state.hpp>
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
Declared in <webcpp/xstate/actors/system_state.hpp>
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 |
webcpp::xstate::system_state::answered
Removes the pending request of a host actor the host has answered.
Synopsis
Declared in <webcpp/xstate/actors/system_state.hpp>
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.
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.
webcpp::xstate::system_state::booked
How many actors the system created or began to create.
Synopsis
Declared in <webcpp/xstate/actors/system_state.hpp>
[[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
Declared in <webcpp/xstate/actors/system_state.hpp>
[[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
Declared in <webcpp/xstate/actors/system_state.hpp>
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
Declared in <webcpp/xstate/actors/system_state.hpp>
[[nodiscard]]
std::set<uint32_t>
family_of(xactor::actor_ref actor) const;
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
Declared in <webcpp/xstate/actors/system_state.hpp>
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.
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
Declared in <webcpp/xstate/actors/system_state.hpp>
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
Declared in <webcpp/xstate/actors/system_state.hpp>
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, |
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
Declared in <webcpp/xstate/actors/system_state.hpp>
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
Declared in <webcpp/xstate/actors/system_state.hpp>
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.
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.
Parameters
| Name | Description |
|---|---|
actor |
The actor. |
webcpp::xstate::system_state::parked
The parked actors.
Synopsis
Declared in <webcpp/xstate/actors/system_state.hpp>
[[nodiscard]]
std::set<uint32_t> const&
parked() const noexcept;
webcpp::xstate::system_state::publish
Keeps the snapshot a machine actor settled in, which actor_system::snapshot_of returns.
Synopsis
Declared in <webcpp/xstate/actors/system_state.hpp>
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
Declared in <webcpp/xstate/actors/system_state.hpp>
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
Declared in <webcpp/xstate/actors/system_state.hpp>
[[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
Declared in <webcpp/xstate/actors/system_state.hpp>
[[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.
Parameters
| Name | Description |
|---|---|
actor |
The actor. |
webcpp::xstate::system_state::requests
The pending requests of the host actors.
Synopsis
Declared in <webcpp/xstate/actors/system_state.hpp>
[[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
Declared in <webcpp/xstate/actors/system_state.hpp>
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
Declared in <webcpp/xstate/actors/system_state.hpp>
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
Declared in <webcpp/xstate/actors/system_state.hpp>
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.
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
Declared in <webcpp/xstate/actors/system_state.hpp>
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
Declared in <webcpp/xstate/actors/system_state.hpp>
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.
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 |
|---|---|
The actions the transition runs. |
|
The transition’s config, which XState spreads into its definition. |
|
A delayed transition’s delay. |
|
The event descriptor: |
|
The index of the transition’s guard in |
|
The config’s |
|
The node the transition belongs to. |
|
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.
webcpp::xstate::transition_definition::config
The transition’s config, which XState spreads into its definition.
webcpp::xstate::transition_definition::delay
A delayed transition’s delay.
webcpp::xstate::transition_definition::event_type
The event descriptor: "" for an eventless (always) transition, the after event’s type for a delayed one.
webcpp::xstate::transition_definition::guard
The index of the transition’s guard in machine::guard.
webcpp::xstate::transition_definition::reenter
The config’s reenter.
webcpp::xstate::transition_definition::source
The node the transition belongs to.
webcpp::xstate::transition_definition::target
The nodes the transition enters, none for a targetless transition.
Synopsis
Declared in <webcpp/xstate/state_node.hpp>
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.
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 |
|---|---|
A config is not a machine, or an implementation or a clock was misused. |
|
A target names no state, or a send names no actor. |
|
A state value, a state id or a path names no state. |
|
A guard the config names has no implementation. |
|
An |
|
An implementation failed, where XState’s would have thrown. |
|
An event has no string |
|
A child actor’s error event reached a machine and no transition took it. |
|
An invoke, or a spawnChild the config names, names no actor of the implementations. |
|
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.
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.
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::implementation_failed
An implementation failed, where XState’s would have thrown.
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.
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.
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.
webcpp::xstate::errc::system_id_taken
An actor claims a systemId that another actor of the system holds.
webcpp::xstate::history_kind
A history node’s kind, from its history member.
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 |
Members
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.
webcpp::xstate::history_kind::shallow
Remembers the parent’s active children.
webcpp::xstate::history_kind::deep
Remembers every active descendant of the parent.
webcpp::xstate::node_type
A node’s type, as XState’s StateNode decides it.
Description
Members
| Name | Description |
|---|---|
A state without children. |
|
A state with children, one of which is active while it is. |
|
A state with children, all of which are active while it is. |
|
A state that, once entered, makes its parent done. |
|
A pseudo-state that enters the children its parent was last in. |
See Also
"States", in the guide.
webcpp::xstate::run_outcome
What a host call left: every actor settled, or one parked for want of fuel.
Members
| Name | Description |
|---|---|
Every actor has done what it was given, and no message is left. |
|
An actor parked because its execution could not pay for its next piece of work, which |
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.
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.
webcpp::xstate::status
A snapshot’s status.
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
See Also
"The snapshot", in the guide.
webcpp::xstate::status::done
The machine reached a final state at the top level, with its output, when it has one.
webcpp::xstate::begin
Makes the cursor of the macrostep an event runs from a snapshot.
Synopsis
Declared in <webcpp/xstate/macrostep.hpp>
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
-
fromis a snapshot ofowner.
See Also
"One microstep at a time", in the guide.
webcpp::xstate::begin_initial
Makes the cursor of a machine’s initial macrostep.
Synopsis
Declared in <webcpp/xstate/macrostep.hpp>
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 ( |
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
Declared in <webcpp/xstate/functions.hpp>
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
-
fromis a snapshot ofowner.
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
Declared in <webcpp/xstate/machine.hpp>
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_configwhen the config is not an object or a member has the wrong type; anentryor anexit, or theactionsof a transition or of an objectinitial, is null, which XState’stoArrayreads as a list that holds null and fails on when it runs it; atags, or a history state’starget, is neither a string nor a list of strings, null included, thetargetof a history at the root too; a compound state has noinitial; anonkey is""; a transition declares a truthycond, renamedguardin XState v5; anafterkey 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 stringsrc; entering a state by default leads back to it; an implementation holds an empty function; an entry ofimplementations::inputsorimplementations::outputsnames what it may not; -
errc::unknown_targetwhen a transition, aninitialor a history state’stargetnames no state; -
errc::unknown_guardwhen a guard the config names is not inimplementations::guards; -
errc::unknown_delaywhen anafterkey names no entry ofimplementations::delays; -
errc::unknown_actorwhen an invoke’ssrc, or thesrcof aspawn_child_actionthe config names, is not inimplementations::actors.
|
Note
|
Nodes are built in document order, a parent before its children, and the children in the order JavaScript enumerates the keys of |
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
Declared in <webcpp/xstate/definition.hpp>
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 |
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
Declared in <webcpp/xstate/actors/message.hpp>
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 |
|
id |
The ending actor’s id, as its parent knows it. |
member |
The payload member that holds |
value |
The output or the error; none for JavaScript’s |
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>
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
Declared in <webcpp/xstate/functions.hpp>
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
Declared in <webcpp/xstate/functions.hpp>
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
Declared in <webcpp/xstate/snapshot.hpp>
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 |
Preconditions
-
ofis a snapshot ofowner.
See Also
"Meta and descriptions", in the guide.
webcpp::xstate::get_microsteps
Runs the macrostep of an event and returns every microstep.
Synopsis
Declared in <webcpp/xstate/functions.hpp>
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
-
fromis a snapshot ofowner.
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
Declared in <webcpp/xstate/functions.hpp>
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
-
fromis a snapshot ofowner.
See Also
"The snapshot alone", in the guide.
webcpp::xstate::get_next_transitions
Lists every transition a snapshot could take, guards not evaluated.
Synopsis
Declared in <webcpp/xstate/functions.hpp>
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
-
fromis a snapshot ofowner.
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
Declared in <webcpp/xstate/functions.hpp>
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
Declared in <webcpp/xstate/state_value.hpp>
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 |
The state value that may lie within |
webcpp::xstate::path_to_state_value
Builds the state value a path describes.
Synopsis
Declared in <webcpp/xstate/state_value.hpp>
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
Declared in <webcpp/xstate/snapshot.hpp>
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
Declared in <webcpp/xstate/definition.hpp>
Writes an event in XState’s flat JSON form.
Writes a machine’s definition, key for key as XState’s machine.toJSON() writes it.
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.
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
Declared in <webcpp/xstate/definition.hpp>
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
Declared in <webcpp/xstate/state_value.hpp>
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
Declared in <webcpp/xstate/state_value.hpp>
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
Declared in <webcpp/xstate/functions.hpp>
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
-
fromis a snapshot ofowner.
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.
See Also
"The xstate.init event", in the guide.
webcpp::xstate::wildcard
The descriptor that matches every event, "*".
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.
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.
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.
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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
a member that is |
the member left out ( |
The rules:
-
create_machinereturns aresult<machine>: the machine, or the reason the config was refused, which the program tests where a program of XState’s would catch whatcreateMachinethrows (Exceptions becomeresult). 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,onandafterare 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), andmachine.toJSON()isto_json(machine), whichcreate_machine_from_definitionloads 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
|
The built-in action of the same name, such as an |
An action that is a plain function, such as
|
No entry: an action name |
A guard, |
A |
A delay, |
A |
|
A |
|
|
An invoke’s |
A |
The rules:
-
Every implementation receives an
action_args:args.context,args.eventandargs.params, XState’s({ context, event }, params). It receives no actor and no system, where XState’s also receivesselfandsystem. -
Every implementation returns a
result, and fails by returning an error where XState’s would throw (Exceptions becomeresult). -
A name is checked when the machine is created. A guard, an
afterdelay or an actor that the config names andimplementationsdoes 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 thatactionsdoes not hold is a custom action, by design. -
paramsare fixed in the config. Where XState computes them with a function of the context and the event, xstate’s implementation reads the two from itsaction_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' }, isxstate::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_jsonand written back byto_json(Events). -
The events xstate makes carry their data in the payload too: the
outputof a done event and theerrorof an error event, each besideactorId, and theinputofxstate.init. XState’sevent.outputis the memberoutputofargs.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_refis 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_ofsays 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 withinvalid_argument(webcpp::xstate::actor_ref). -
XState’s
actor.getSnapshot()returns a snapshot.snapshot_of(actor)returns a pointer to one,nullptruntil the actor has started (The root is constructed onstart). -
XState’s
actor.sendreturns nothing.system.send, like every call that delivers something, runs the system and returns aresult<run_outcome>:settled,out_of_fuelor 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.childrenholds its children’s references. xstate’s holds their ids and srcs, andchild_of(actor, id)returns a child’s reference; XState’sactor.system.get(systemId)issystem.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 |
|---|---|
|
|
The promise function’s |
The request’s |
The promise resolves with a value |
|
The promise rejects with an error |
|
|
The members |
A promise that settles after its actor has stopped is ignored |
An answer to a request no longer pending is refused with
|
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 |
|---|---|
|
|
|
|
|
|
|
No error callback: |
The |
The member |
The rules:
-
A
resultis tested before it is read.*reads the value of a result that holds one, andvalue()of a failure callsboost::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
atandas_int64, have counterparts that answer instead:if_containsandif_int64with a pointer,try_atandtry_as_int64with aresult(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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
None in the interface: the |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
the |
|
|
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:
-
What JSON imposes: a machine is JSON, which holds no function, no
undefinedand no reference to an actor. -
What the actor model on xactor imposes: deliveries are queued instead of nested in the sender’s call, actors pay for their work with fuel, time is what the host brings, the actors of one
actor_systemlive in one system, and a host’s callback runs inside an actor’s turn. -
Where XState leaks JavaScript or has a defect: an accident of the language or of an implementation, which xstate does not reproduce.
-
Where imitating XState would make the API worse: failures as values, a config checked when it is loaded, a clock that hands its caller the due events, and one kind of listener of emitted events.
-
The checks of XState’s development build, which xstate always makes.
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,sendToand the others),params, an input or an output computed from the context, a context computed from the input, and logic given as an invoke’ssrc. - xstate
-
Every function is an implementation registered by name in
implementations, as XState’ssetup()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 actiona custom action: a name
actionsdoes not hold, which the core returns and the actor layer hands toon_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 theiraction_args; a custom action carries itsparamsalone (Custom actions)src: someMachine,src: fromPromise(...)the name of an entry of
actors(An invoke’ssrc) - 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,andandorare functions that wrap other guards, andstateInone that tests a state value. JSON cannot hold them, andmachine.toJSON()writes the transition without its guard. - xstate
-
They are JSON objects. The
typenames the combinator with the prefix XState gives its built-in actions,xstate., and the other members are the properties XState’s functions carry,guardsandstateValue:{"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_jsonwrites 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';
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)}`);
SUBMIT without a name: "editing" SUBMIT with a name: "submitted" the definition's guard: undefined
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, aspawnfunction and the actor itself, and a context may hold an actor it spawned. - xstate
-
implementations.contextreceives the input only (Initial context from the input). A context is JSON and holds no actor; a child is spawned by aninvokeor aspawn_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.spawnChildaction for its caller to run, which the actor layer does (Spawning actors).
An invoke’s src
- XState
-
srcis the name of an actorsetup()registered, or the logic itself: a machine, orfromPromise(...). - xstate
-
srcis a string that names an entry ofimplementations.actors. Any othersrcrefuses the machine withinvalid_config, and a name with no entry withunknown_actor(A config is refused when it is created). Ahost_actorstands forfromPromise: the host answers it (Host actors). - Why
-
Logic cannot be JSON.
Targets that name actors
- XState
-
sendTotakes an actor reference, a string (a child’s id,#_parent,#_internal,#_<id>) or a function that returns one; a send to a systemId is writtensendTo(({ system }) => system.get('notifier'), ...). An event or an input may carry an actor reference, as a parent passesselfto 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, asendTogoes to the actor itself, as XState’s does when the function returnsundefined, and aforwardTofails (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.actionsdoes not hold is a custom action (Custom actions). The machine core returns it as{type, params}, as XState’s puretransition()returns the actions to execute, and the actor layer hands each one, in order, to the callbackon_actionregisters, with the actor that returned it, and nothing else; a log reacheson_logthe 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 fixedparams, or reach the host through anemit_action, whoseevent_makerreads 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
undefinedfromnull. The eventxstate.initalways has a memberinput,undefinedwhen the actor was given none, and an implementation that returnsundefinedleaves a log without a value, a child without an input or a snapshot without an output. - xstate
-
JSON has
nullonly. Avalue_maker, the implementation that computes a log’s value, an input or an output, returns aresult<std::optional<boost::json::value>>, and none, the empty optional, stands forundefined: the member is left out, as XState leaves it out (When a value is undefined).xstate.initcarriesinputonly 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
metathat is null is kept:getMeta()lists it, andmachine.toJSON()writes it. A state’sdescriptionis kept whatever its type, and written too, although XState’s own type declarations make it a string. - xstate
-
A null
metais no meta:get_metadoes not list it, andto_jsonleaves it out. Adescriptionthat is not a string is left out of the machine, and so of its definition (What creation refuses). A transition’smetaanddescriptionare kept as XState keeps them. - Why
-
A
nullis 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, andstate_node::descriptionholds 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;
}
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();
kidStarted appStarted
appStarted kidStarted
Fuel
- XState
-
Nothing bounds a call unless the config asks for it:
createActor,start()andsend()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 iscreateActor, which runs the macrostep (The root is constructed onstart). The bound XState has is the config’soptions.maxIterations: once that many microsteps have followed the event’s own with the machine still active, the macrostep throws an error whose message beginsInfinite loop detected, even when it would settle there.transitionlets 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’sstart,send,clock_tick,resolveandrejectare held, in order, and applied by a laterresume(), so no event starts before the macrostep in progress has settled.stopis never held: it is how the host ends an actor that would never settle. xstate does not readoptions. - 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_snapshotamong them, run without a bound, as XState’s do when the config sets none; a caller that must bound the work drives themacrostepcursor itself, one microstep pernext()(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';
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);
start(): error stop(): error
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:
transition throws: Infinite loop detected the stable state is still "idle"
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:
transition throws: Infinite loop detected
"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
-
createActorconstructs the root at once: its initial macrostep runs,getSnapshot()returns its snapshot, and its system holds the systemIds of its initial children, all beforestart(). - xstate
-
create_actorcreates the actor and runs nothing. Its construction runs onstart, so until thensnapshot_ofreturnsnullptrandgetfinds none of its children’s systemIds; its initial children also take their session numbers then (One system peractor_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_actordelivers 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);
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);
before start: "running", worker registered after start: "running", worker registered
before start: no snapshot, worker unknown after start: "running", worker registered
One system per actor_system
- XState
-
createActormakes a system of its own for every root: its systemIds,system.getand the sends to a systemId. An actor’s session id, its id when it is given none, isx:<n>, wherencounts every actor the process created, across all its systems. - xstate
-
The roots of one
actor_systemshare 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, andcreate_actorrefuses one too, withsystem_id_taken. A root’s session id counts the actors its ownactor_systemcreated before it, a spawn whose claim of a systemId was refused included, so the two agree when a process runs oneactor_systemwith 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 anactor_systemof its own (The system). - Why
-
An
actor_systemowns 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
-
SimulatedClockand the delays take fractions of a millisecond. Anafterkey 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_tickmoves, and the core’ssimulated_clockcount whole milliseconds, and a delay is astd::uint64_t. Anafterkey 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 withinvalid_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.setthrows for a time before the current one, and the time stays where it was. - xstate
-
clock_tickaccepts 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’ssimulated_clockrefuses 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
nowback (xactor’s guarantee 18 and guarantee 34). The actor layer, whose timers are xactor’s, keeps that rule as it is. The core’ssimulated_clockis a port of XState’sSimulatedClock, 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';
}
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)}`);
}
set(10) throws: Unable to travel back in time at 1050: "green" at 1100: "yellow"
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.onlistener 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
subscribeoron_emittedlistener,on_action,on_logand the inspector, run inside an actor’s turn, while the system runs. A call from one of them to a member ofactor_systemthat 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’serrc::invalid_argument, of the categorywebcpp.xactor, and changes nothing. The const members, which read the system,snapshot_of,status_of,getand 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");
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)}`);
heard "a" heard "b" the listener sent NEXT heard "c" after the host's send: "c"
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 asendParent, 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.sendToto#_parent. Delivering it is the caller’s work. In the actor layer, a root that sends to its parent fails withunknown_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';
}
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}`);
}
transition() throws: Unable to send event to actor '#_parent' from machine 'child'.
xstate.sendTo {"targetId":"#_parent","event":{"type":"DONE"}}
An emitted event of the type *
- XState
-
emithands 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_emittedhears every event an actor emits, as XState’sactor.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;
}
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' });
heard * heard *
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 astopChildof it then leaves that key in the snapshot’s children. - xstate
-
The id of a
spawn_child_actionis fixed or computed by anid_maker, and one left out is the empty string: the child is keyed under"", and astop_child_actionof""removes it from the snapshot’s children. - Why
-
A C++ string has no undefined: an
idleft 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';
}
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}`);
}
["undefined"] first: active, keyed: false second: active, keyed: true
[""] first: active, keyed: false second: active, keyed: true
Equal deadlines
- XState
-
SimulatedClocksorts 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_clockand in the actor layer’sclock_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
cancelAllcancels the timers its_scheduledEventslists: 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
-
SimulatedClockfires 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 bynowone at a time, in the order of their deadlines, and a timer armed meanwhile and due bynowis 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’sstopChildof a failed child keeps that child’s children running, as XState does. - Why
-
The host’s
stopis 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
sendTothat 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 toundefined, which throws aTypeError. - xstate
-
The actor fails with
unknown_targetwhen 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
contextin the config,null,0,falseor"", starts the machine with{}, and any other is kept. Anassignmerges withObject.assign, which spreads a string or an array into numbered members, so"ab"becomes{"0": "a", "1": "b"}, and leaves out a number ortrue; whatever a context function returns is merged into an empty object the same way. - xstate
-
A config whose
contextisnull, 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 anassignon it fails withimplementation_failed, where XState starts the machine with{}for a falsy one and merges the update into any other withObject.assign(Initial context). A context function,implementations.context, returns aresult<boost::json::value>: a value that is not an object fails the initial macrostep withimplementation_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
nullis 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';
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))}`);
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"}
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()andgetNextSnapshot()let the error out;getInitialSnapshot()and an actor catch it into the snapshot, whosestatusis'error', with the value and the context from before the event. A child that fails sends its parentxstate.error.actor.<id>, whoseerroris the value the implementation threw. - xstate
-
An implementation returns a
result. A failure,implementation_failedor any othererror_codethe implementation returns, settles the macrostep with the snapshot it began from, itsstatuserrorand itserrorthat 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 parentxstate.error.actor.<id>, whoseerroris the child’serror_valuewhen 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 inerror, and the error it reported inerror_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';
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}`);
transition() throws: count failed an actor's snapshot: error, "idle", count failed
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:
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')
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:
ran note logged: say status: error (implementation_failed) value: "idle"
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:
shop {"type":"xstate.error.actor.checkout","error":{"code":"declined"},"actorId":"checkout"}
checkout: error {"code":"declined"}
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
-
createMachineaccepts configs it fails on later, or runs wrongly: aninitialthat names no child throws when the machine starts; a guard with no implementation throws when an event first evaluates it; a namedafterdelay 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; anentry,exitoractionsthat is null, whichtoArrayreads as a list that holds null, throws aTypeErrorwhen that list runs, and a history’stargetthat is null, read the same way, when the default is needed; an invoke of an actorsetup()does not hold warns, in the development build, and fails when something reads the child. - xstate
-
create_machinerefuses 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 thatcreateMachineaccepts, such as a history target that is not a string or a list of strings, the root’s too, and atagsthat 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 withunknown_targetwhere 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);
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}`;
});
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')
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:
Unable to evaluate guard 'isValid' in transition for event 'submit' in state node '(machine).editing': Guard 'isValid' is not implemented.'.
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:
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)
invalid_config unknown_target
The core’s clock hands over the due events
- XState
-
SimulatedClockholds functions:setTimeoutkeeps one with its delay, andsetandincrementmove the time and call each function that falls due, in the order of the deadlines.setthrows for a time before the current one. - xstate
-
The core’s
simulated_clockholds events.applyschedules each built-in delayedxstate.raisea step returns and applies each built-inxstate.cancel, a custom action being the caller’s whatever its type or its params, andscheduleholds any event.setandincrementonly move the time, andpop_due()takes out the earliest event that is due, which the caller delivers as a macrostep of its own.setrefuses a time before the current one withinvalid_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_emittedadds a listener that hears every event the actor emits, as XState’sactor.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;
}
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' });
the listener of PING heard PING the listener of every type heard PING
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=developmentselects, 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 withinvalid_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 gaveguard, refuses the machine withinvalid_configwhen thecondis truthy, as JavaScript reads it. -
A
forwardTowhose target resolves to no actor fails withunknown_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 isundefinedto 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';
}
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}`);
}
}
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.
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:
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 ('*')
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, andto_jsonwrites a machine’s definition as XState’smachine.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
objectkeeps its members in the order they were written, which xstate needs to readstates,onandafterin 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 JSONparams. A machine written that way keeps its config unchanged in xstate (Functions become implementations registered by name). -
Names are checked once. A guard, an
afterdelay 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_codecarries its category, compares with the codes ofxstate::errcandxactor::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.maxIterationsis 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/examplebuilds 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 inimplementations.actions, and anxstate::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 anActorRef; what it runs is its actor logic.
xstate: an actor of anxstate::actor_system, named by anxstate::actor_ref;create_actorandstartfor a root;machine_actorandhost_actorfor what a child runs, by itssrc.
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:
createActorand the system it creates, which XState’s package holds beside its machines.
xstate:<webcpp/xstate/actors.hpp>, whosexstate::actor_systemholds 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_fuelwhen 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, withsystem.get(systemId).
xstate:xstate::actor_system, built with the fuel of one execution (xstate::budgets); the roots created in oneactor_systemshare one system, where XState makes one for each root (One system peractor_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.assignmerges 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, whoseassignment, anxstate::assigner, returns the partial update as aresult<boost::json::object>; an assign on a context that is not an object fails withimplementation_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
greenin a traffic light. Each active branch of a machine ends in an atomic state or a final one.XState:
type: 'atomic', which a state withoutstateshas.
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 withxstate.).XState: the action creators
assign,raise,sendToand the rest.
xstate: the alternatives ofxstate::action_implementation, registered by name inimplementations.actions; a returned one hasaction::builtinset.
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)andparent_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_clockthe 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 withclock_tick(now). Time counts whole milliseconds.XState: the
clockoption ofcreateActor,setTimeoutby default, andSimulatedClockfor 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
- 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: aboost::json::value, read bycreate_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 asfalse, becomes{}, and what a context function of the input returns is merged into{}.
xstate:snapshot::context, aboost::json::value. A config’s context that is not an object, null aside, is kept as written, and an assign on it fails withimplementation_failed. The context function isimplementations.context, acontext_maker, which returns aresult<boost::json::value>; a value that is not an object fails the initial macrostep withimplementation_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()oractor.send().
xstate:xstate::macrostep, frombegin(machine, snapshot, event)orbegin_initial(machine, input);next()returns aprogress(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: anxstate::actionwhosebuiltinis false; in the actor layer, handed to theon_actionlistener.
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 actionxstate.raiseorxstate.cancel: a definition cannot tell such an action from the raise and cancel anafteradds, 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, andcreate_machine_from_definition(definition, implementations, context), which refuses withinvalid_configa 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:
raiseandsendTowith{ delay, id },cancel(id), andsetup({ delays }).
xstate: thedelayandidofraise_actionand of the send actions,cancel_action, andimplementations.delays. The machine returns the action. With the machine core, asimulated_clockholds 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
afteradds, on entry, a delayed raise of the eventxstate.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 sameafterin the config; entering the state returns anxstate.raisewith adelay, and leaving it anxstate.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=developmentselects.
xstate:invalid_eventfor an event of the type*a macrostep takes from outside,invalid_configfor a transition with a truthycond, andunknown_targetfor 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’sorder, 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’sonandafterthe same way as those ofstates, and XState and xstate read them so: a state whoseafterwrites1000before500returns on entry the raise of500before that of1000.XState:
StateNode.order, fromstatesread withObject.keys.
xstate: the index of a node, as inmachine::node(index);state_node::order, XState’sorder, 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’sonDonetakes it. When a child actor is done, its parent receivesxstate.done.actor.<child id>, with the child’soutputand itsactorId, and the invoke’sonDonetakes it.XState: the same two events.
xstate: the same; anonDoneis 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 withactor.on(type, handler).
xstate:emit_action, returned asxstate.emit; in the actor layer, heard withactor_system::on_emitted(actor, listener), which hears every type, as XState’sactor.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 aboost::system::error_code: anxstate::errc, of the categorywebcpp.xstate, or xactor’serrc::invalid_argument, of the categorywebcpp.xactor, whichxstate::errcdoes not have.actor_systemreturnsinvalid_argumentfor astart, asend, astop, astatus_of, asubscribeor anon_emittedwhose 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_actorrefuses a systemId another running actor holds withxstate::errc::system_id_taken. The const readerssnapshot_of,child_of,parent_of,id_ofandgetrefuse 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 bycreate_machine, and an implementation that fails leaves the snapshot in the statuserror, with its error code.XState: an exception, or a snapshot whose
statusis'error'.
xstate:xstate::result<T>, the codes ofxstate::errc, such asinvalid_config,unknown_targetandsystem_id_taken, andsnapshot::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’serrorand itsactorId. The invoke’sonErrortakes it. When no transition takes it, the parent fails in turn: its statuserror, its error codeactor_failed, and the child’s error insnapshot::error_value.XState: the same event; an unhandled one puts the parent’s snapshot in the status
'error'.
xstate: the same, with the error codeactor_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_jsonreads XState’s flat form andto_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.*takesmouse.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 withinvalid_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
alwaystransitions 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, forresume, 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 acorrelation_id, whose fuelfuel_of(correlation)tells;xactor::budgets, whichxstate::budgetsnames, 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::doneandsnapshot::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::fuelin the actor layer, withrun_outcome::out_of_fuelandresume().
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 anchorsxstate-invariant-<n>andxstate-invariant-a<n>.
xactor: the anchorsxactor-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,orandstateIn, are functions in XState; xstate writes them as JSON.XState:
guard: 'isValid', implemented insetup({ guards }), andnot(...),and([...]),or([...]),stateIn(...).
xstate:"guard": "isValid", implemented inimplementations.guardsas apredicate, and{"type": "xstate.not", "guards": [...]}and its siblings. A guard the implementations do not hold is refused at creation withunknown_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'withhistory: 'shallow'or'deep', or, on a state withouttypeor children, a truthyhistoryalone;snapshot.historyValue.
xstate: the same in the config, where ahistoryoffalse,null,""or0is 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_argumentand changes nothing (listener).XState: the code that calls
createActor,actor.start()andactor.send().
xstate: the caller of the memberscreate_actor,start,send,clock_tick,resolve,reject,resumeandstopofactor_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 inimplementations.actorsand answered withactor_system::resolveorreject.
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
fromPromisecreator, with itsinput.
xstate:xstate::host_request, listed byactor_system::host_requests();resolveandrejectrefuse one no longer pending with xactor’sinvalid_argument.See Host actors.
- 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 tocreate_machine: the mapsactions,actors,guards,delays,inputs(by invoke id) andoutputs(by state id), andcontext.
See Implementations. - Initial state
-
The child a compound state enters first, which its
initialnames. 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 eventxstate.init, which carries a non-null input as its memberinput.XState:
initial;getInitialSnapshotandinitialTransition.
xstate:initial;begin_initial(machine, input), andget_initial_snapshotandinitial_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’sinput, and the context function({ input }) => ....
xstate:actor_options::inputandbegin_initial(machine, input); an invoke’sinputin the config orimplementations.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
inspectoption ofcreateActor, with inspection events such as@xstate.actor,@xstate.snapshotand@xstate.event.
xstate:xstate::inspector(started,settled,emitted), given toactor_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
pingtwice toa, then once tob, hasbhandle its event between the two thatahandles, 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
onDoneandonErrortake the child’s done and error events.XState:
invokewithsrc,id,input,systemId,onDone,onErrorandonSnapshot.
xstate: the same in the config, itssrca name ofimplementations.actors; itsonDone,onErrorandonSnapshotare transitions onxstate.done.actor.<id>,xstate.error.actor.<id>andxstate.snapshot.<id>, and the actor layer sends no snapshot event yet. Entering the state returnsxstate.spawnChildand leaving itxstate.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_ofandsnapshot_oftell 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’sinvalid_argumentand 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(...), whoseerrorcallback hears a failure, andactor.on(...); a running actor runs custom actions and logs itself.
xstate:actor_system::subscribe,on_emitted,on_actionandon_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, fromcreateMachine(config)orsetup({ ... }).createMachine(config).
xstate:xstate::machine, fromcreate_machine(config, implementations), which returns aresult<machine>.
See Machines. - Machine actor
-
An actor that runs a machine: a root the host creates from a machine, or a child whose
srcnames 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 whosesrcnames a machine insetup({ actors }).
xstate:actor_system::create_actor(machine, options), and anxstate::machine_actorinimplementations.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,
transitionand 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()oractor.send()runs;macrostepis the name of its internal function, after SCXML.
xstate:xstate::macrostep, the cursor that runs one, andmacrostep_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
descriptionis a string that documents a state for the people who read the machine. A transition may have both too.XState:
metaanddescription;snapshot.getMeta().
xstate: the same in the config;get_meta(machine, snapshot), themetaof every active node that has one, by node id;state_node::metaandstate_node::description. The definition writes them, except a state’s nullmetaand a state’sdescriptionthat is not a string, which xstate leaves out (A state’s nullmeta, and adescriptionthat 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 ofgetMicrosteps(), and the inspection event@xstate.microstep.
xstate:xstate::microstep(event,transitions,actions,snapshot), fromget_microstepsor from onenext()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, andtest/oracle/actors.mjs, which runs the actor cases;b2 libs/xstate/test/oracle//oracleruns 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:
outputon a final state or on the machine;snapshot.output.
xstate:outputin the config, orimplementations.outputsby 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 ofsnapshot::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:paramsin the config, always a fixed value, since JSON holds no function;action_args::params, null when the config gives none, andaction::paramson 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
initialnames. A compound state withoutinitialis refused at creation.XState: compound states, with
statesandinitial.
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’sstart,send,clock_tick,resolveandrejectare checked, held in the order made, and returnout_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 returnsrun_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_statecompletes the value to every node it implies, with the statusdonewhen the value puts the machine in a final state andactiveotherwise, nevererror. The output, the error, what history states remember and the children are not restored.XState:
machine.resolveState({ value, context }), often of whatJSON.stringify(snapshot)wrote; and an actor’sgetPersistedSnapshot(), whichcreateActor(machine, { snapshot })restores with what its history states remember.
xstate:resolve_state(machine, value, context), which returns aresult<snapshot>and refuses a value that names no state withunknown_state. xstate has no counterpart of an actor’s persisted snapshot: what history states remember lives only in anxstate::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 asxstate.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
createActorreturns,actor.stop()andactor.sessionId.
xstate:actor_system::create_actorandstop(root);parent_ofholds nothing for it, andid_ofgives 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::targetandreenter.
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)andforwardTo(target); a target may be a function, such as({ system }) => system.get(id).
xstate:send_to_action,send_parent_actionandforward_to_action, each returned asxstate.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::settledandmacrostep::done(); in the actor layer,run_outcome::settledsays 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, fromgetInitialSnapshot,transitionoractor.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 }), andspawninside anassign.
xstate:spawn_child_action(src,id,system_id,input), returned asxstate.spawnChild; a spawn without an id keys the child under"", and xstate has nospawninside 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 ofstatesin 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)andmachine::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)andmatchesState.
xstate:snapshot::value, aboost::json::value;snapshot::matchesandmatches_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; orerror, 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 addsstopped.XState:
snapshot.status:'active','done','error'or'stopped'.
xstate:snapshot::status, anxstate::status;actor_system::status_of, anxactor::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, andcreate_actorrefuses it.XState:
systemIdon an invoke, a spawn orcreateActor, andsystem.get(systemId).
xstate:systemIdon an invoke,spawn_child_action::system_idandactor_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, andsnapshot.hasTag(tag).
xstate:tagsin the config;snapshot::tagsandsnapshot::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 withunknown_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: trueit leaves the source and enters it again, so the source’s exit and entry actions run and its invoked actors restart.reenterchanges nothing for a transition without a target, and a transition to another state leaves its source either way.XState:
on: { event: 'target' }, ortarget,guard,actionsandreenter.
xstate: the same in the config;xstate::transition_definition, which a microstep’stransitionspoint 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’stools/oracle/twins.pyruns 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 astd::optionalwith none, and the member is left out, as XState leaves it out. The eventxstate.initcarriesinputonly when the input is not null.XState:
undefined.
xstate: none in the result of avalue_maker, inhost_request::inputand insnapshot::output; a null pointer for the valueon_logreceives.
Seeundefinedin 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.spawnChildandxstate.stopChildactions 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 undertest/oracle/, is what the oracle and the twins run, on its development build, as XState’s own tests do.See Overview and How xstate is tested.
- 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
createMachinetakes, 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, underinclude/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 |
|---|---|---|
|
The event of a machine’s initial macrostep, with a non-null input as its
member |
|
|
The event a state’s |
|
|
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. |
|
|
Sent to the parent when its child |
|
|
Sent to the parent when its child |
|
|
The event an invoke’s |
|
|
A raise, returned with the params |
|
|
A sendTo, a sendParent or a forwardTo, with |
|
|
A cancel, with the |
|
|
A log, with its |
|
|
A spawn, or an invoke’s start, with |
|
|
A stopChild, or an invoke’s stop, with the |
|
|
An emit, with the |
|
|
The JSON of XState’s higher-order guards, which XState writes as functions. |
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
checkoutis a macrostep of two microsteps:#1takes the event’s transition tochecking,#2the eventless transition back tocart, sincehasItemsis false. Nothing follows, so#2settles it. -
addchanges the context through the assigncount, and returns no action: an assign is never handed back. -
The second
checkouttakes three microsteps.#2is the eventless transition topaying, whose entry returns the custom actioncharge, for the caller to run, andxstate.raise, the raise ofpaid, already queued.#3takes the raised event toclosed, a final state: the status isdone. -
settledis what the cursor reports with the last microstep of each macrostep. XState has no cursor: itsgetMicrostepsreturns every microstep of a macrostep at once, and the program’s twin marks the last one, so that it prints the same lines (twin). -
countandhasItemsreaditemsthroughitems_of: on a context without an integeritems, each fails withimplementation_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 childfetch. 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. -
resolveanswers the request: the child is done, and the app receives its done event,xstate.done.actor.fetch, whoseoutputthe assignkeepUserkeeps. Leavingloadingstops 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_EXCEPTIONSconfiguration macro, defined automatically or by the developer. -
Added a changelog, the page’s last appendix, with each version’s changes.
-
Changed
config.hppto 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.