Documentation
¶
Overview ¶
Package engine builds and runs statecharts.
CreateMachine validates a MachineConfig into an immutable Machine, and Setup does the same with guards and actions registered by name. An Actor runs a machine: Actor.Start it, feed it events with Actor.Send, read it with Actor.Snapshot, and store or restore it with Actor.Persist and NewActorFromSnapshot.
The engine performs no I/O and reads no clock. Delayed transitions and invocations are exposed as data (Actor.PendingTimers, Actor.PendingInvocations) for a host to drive (Actor.FireTimer, Actor.ResolveInvocation, Actor.RejectInvocation).
The other packages hold the vocabulary a machine is written in: action for actions, guards and conditions, effect for timers and invocations, persist for the active configuration and snapshots, and describe for the type-erased view that tooling reads.
Example ¶
Example builds a one-state counter machine, drives it with two events, and reads the accumulated context.
package main
import (
"context"
"fmt"
"github.com/arisros/fate/action"
"github.com/arisros/fate/engine"
)
func main() {
type Ctx struct{ Count int }
m, err := engine.CreateMachine(engine.MachineConfig[Ctx, string]{
ID: "counter",
Initial: "active",
States: map[string]engine.StateNodeConfig[Ctx, string]{
"active": {On: map[string][]engine.TransitionConfig[Ctx, string]{
"INC": {{Actions: []action.Action[Ctx, string]{
action.Assign(func(c Ctx, _ string) Ctx { c.Count++; return c }),
}}},
}},
},
})
if err != nil {
panic(err)
}
a := engine.NewActor(m)
_ = a.Start(context.Background())
_ = a.Send(context.Background(), "INC")
_ = a.Send(context.Background(), "INC")
fmt.Println(a.Snapshot().Context.Count)
}
Output: 2
Example (DelayedTransition) ¶
Example_delayedTransition shows the clock-agnostic timer model: the engine records a pending "after" timer but never fires it. A driver (here, the test itself; in production the fate/temporal adapter) decides the delay elapsed and calls FireTimer.
package main
import (
"context"
"fmt"
"time"
"github.com/arisros/fate/engine"
)
func main() {
m, _ := engine.CreateMachine(engine.MachineConfig[struct{}, string]{
ID: "blink",
Initial: "off",
States: map[string]engine.StateNodeConfig[struct{}, string]{
"off": {After: map[time.Duration][]engine.TransitionConfig[struct{}, string]{
time.Hour: {{Target: "on"}},
}},
"on": {Type: engine.NodeFinal},
},
})
a := engine.NewActor(m)
_ = a.Start(context.Background())
fmt.Println(a.Snapshot().Value.Path())
// A driver pulls the pending timer and fires it once the delay elapses.
a.FireTimer(a.PendingTimers()[0].ID)
fmt.Println(a.Snapshot().Value.Path())
}
Output: off on
Example (Meta) ¶
Example_meta attaches host data to a state and a transition and reads it back from the descriptor, where a form builder or a viewer would find it.
package main
import (
"fmt"
"github.com/arisros/fate/engine"
)
func main() {
type state = engine.StateNodeConfig[struct{}, string]
type transition = engine.TransitionConfig[struct{}, string]
m, err := engine.CreateMachine(engine.MachineConfig[struct{}, string]{
ID: "task",
Initial: "survey",
States: map[string]state{
"survey": {
Meta: map[string]any{"form": "survey_form"},
On: map[string][]transition{
"SUBMIT": {{Target: "done", Meta: map[string]any{"title": "Submit", "order": 1}}},
},
},
"done": {Type: engine.NodeFinal},
},
})
if err != nil {
panic(err)
}
survey := m.Describe().States["survey"]
fmt.Println(string(survey.Meta))
fmt.Println(string(survey.On["SUBMIT"][0].Meta))
}
Output: {"form":"survey_form"} {"order":1,"title":"Submit"}
Example (Persistence) ¶
Example_persistence shows that an actor round-trips through a JSON snapshot: the restored actor continues from exactly where the original left off.
package main
import (
"context"
"fmt"
"github.com/arisros/fate/action"
"github.com/arisros/fate/engine"
)
func main() {
type Ctx struct{ Count int }
build := func() *engine.Machine[Ctx, string] {
m, _ := engine.CreateMachine(engine.MachineConfig[Ctx, string]{
ID: "counter",
Initial: "active",
States: map[string]engine.StateNodeConfig[Ctx, string]{
"active": {On: map[string][]engine.TransitionConfig[Ctx, string]{
"INC": {{Actions: []action.Action[Ctx, string]{
action.Assign(func(c Ctx, _ string) Ctx { c.Count++; return c }),
}}},
}},
},
})
return m
}
a := engine.NewActor(build())
_ = a.Start(context.Background())
_ = a.Send(context.Background(), "INC")
blob, _ := a.Persist()
restored, _ := engine.NewActorFromSnapshot[Ctx, string](build(), blob)
_ = restored.Send(context.Background(), "INC")
fmt.Println(restored.Snapshot().Context.Count)
}
Output: 2
Example (Tooling) ¶
Example_tooling annotates a guard with Gates and a state with UIStateOf, then reads both the way a viewer would: the gate from the descriptor, the view model from the live snapshot.
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/arisros/fate/action"
"github.com/arisros/fate/describe"
"github.com/arisros/fate/engine"
)
func main() {
type Ctx struct {
Score int `json:"score"`
}
type ReviewView struct {
Score int `json:"score"`
Passes bool `json:"passes"`
}
m, err := engine.CreateMachine(engine.MachineConfig[Ctx, string]{
ID: "review",
Initial: "pending",
Context: Ctx{Score: 72},
States: map[string]engine.StateNodeConfig[Ctx, string]{
"pending": {
UIState: describe.UIStateOf(func(c Ctx) ReviewView {
return ReviewView{Score: c.Score, Passes: c.Score >= 60}
}),
On: map[string][]engine.TransitionConfig[Ctx, string]{
"DECIDE": {{
Target: "approved",
Guard: func(c Ctx, _ string) bool { return c.Score >= 60 },
CondMeta: action.Gates(action.Field("$.score").Gte(60)).Sample(`{"score":60}`),
}},
},
},
"approved": {Type: engine.NodeFinal},
},
})
if err != nil {
panic(err)
}
gate, _ := json.Marshal(m.Describe().States["pending"].On["DECIDE"][0].CondMeta)
fmt.Println(string(gate))
a := engine.NewActor(m)
_ = a.Start(context.Background())
s := a.Snapshot()
views, _ := m.UIState(s.Value, s.Context)
fmt.Println(string(views["pending"]))
}
Output: {"fields":[{"path":"$.score","op":"gte","value":60}],"sample":{"score":60}} {"score":72,"passes":true}
Index ¶
- Variables
- type Actor
- func (a *Actor[Ctx, Evt]) Can(evt Evt) bool
- func (a *Actor[Ctx, Evt]) Enabled(byName func(name string) (Evt, bool)) []string
- func (a *Actor[Ctx, Evt]) FireTimer(id effect.TimerID) bool
- func (a *Actor[Ctx, Evt]) NextEvents() []string
- func (a *Actor[Ctx, Evt]) PendingInvocations() []effect.PendingInvocation
- func (a *Actor[Ctx, Evt]) PendingTimers() []effect.PendingTimer
- func (a *Actor[Ctx, Evt]) Persist() ([]byte, error)
- func (a *Actor[Ctx, Evt]) Preview(evt Evt) (persist.Snapshot[Ctx], error)
- func (a *Actor[Ctx, Evt]) RejectInvocation(id effect.InvokeID, err error) bool
- func (a *Actor[Ctx, Evt]) ResolveInvocation(id effect.InvokeID, output any) bool
- func (a *Actor[Ctx, Evt]) Send(_ context.Context, evt Evt) error
- func (a *Actor[Ctx, Evt]) Snapshot() persist.Snapshot[Ctx]
- func (a *Actor[Ctx, Evt]) Start(_ context.Context) error
- func (a *Actor[Ctx, Evt]) Stop()
- func (a *Actor[Ctx, Evt]) Subscribe(obs func(persist.Snapshot[Ctx])) func()
- func (a *Actor[Ctx, Evt]) SubscribeSteps(obs func(Step)) func()
- type ActorOption
- type Finding
- type FindingKind
- type History
- type Machine
- func (m *Machine[Ctx, Evt]) Describe() describe.MachineDescriptor
- func (m *Machine[Ctx, Evt]) ID() string
- func (m *Machine[Ctx, Evt]) IsKnownState(name string) bool
- func (m *Machine[Ctx, Evt]) IsLegalTransition(from string, eventName string) bool
- func (m *Machine[Ctx, Evt]) IsTerminal(name string) bool
- func (m *Machine[Ctx, Evt]) Lint() []Finding
- func (m *Machine[Ctx, Evt]) States() []string
- func (m *Machine[Ctx, Evt]) UIState(v persist.StateValue, ctx Ctx) (map[string]json.RawMessage, error)
- type MachineConfig
- type NodeType
- type SelectedTransition
- type Setup
- func (s *Setup[Ctx, Evt]) Action(name string) action.Action[Ctx, Evt]
- func (s *Setup[Ctx, Evt]) CreateMachine(cfg MachineConfig[Ctx, Evt]) (*Machine[Ctx, Evt], error)
- func (s *Setup[Ctx, Evt]) Guard(name string) action.Guard[Ctx, Evt]
- func (s *Setup[Ctx, Evt]) WithAction(name string, a action.Action[Ctx, Evt]) *Setup[Ctx, Evt]
- func (s *Setup[Ctx, Evt]) WithGuard(name string, g action.Guard[Ctx, Evt]) *Setup[Ctx, Evt]
- type StateNodeConfig
- type Step
- type StepCause
- type StepTransition
- type TransitionConfig
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrInvalidConfig is returned by CreateMachine when the supplied config // fails validation. The wrapped error gives the specific reason. ErrInvalidConfig = errors.New("statechart: invalid machine config") // ErrUnknownTarget is returned when a transition's Target string does not // resolve to any sibling, descendant, or ancestor state path. ErrUnknownTarget = errors.New("statechart: unknown transition target") // ErrNoInitial is returned when a compound state node lacks an Initial // field naming one of its children. ErrNoInitial = errors.New("statechart: compound state has no initial child") // ErrUnknownInitial is returned when an Initial field names a state that // is not among the node's children. ErrUnknownInitial = errors.New("statechart: initial state not found among children") // ErrDuplicateState is returned when two sibling states share a name. ErrDuplicateState = errors.New("statechart: duplicate sibling state name") // ErrInvalidNodeType is returned when a state node has a Type the current // skeleton does not yet support (e.g. NodeParallel, NodeHistory, NodeFinal // before P5). ErrInvalidNodeType = errors.New("statechart: state node type not supported in this build") // ErrSnapshotMismatch is returned by NewActorFromSnapshot when the // snapshot's state value is not a configuration of the supplied machine, // typically because the machine changed after the snapshot was taken. ErrSnapshotMismatch = errors.New("statechart: snapshot does not match machine") // ErrActorNotStarted is returned by Send when the actor's Start has not // been called yet. ErrActorNotStarted = errors.New("statechart: actor not started") // ErrActorStopped is returned by Send when the actor has been Stopped. ErrActorStopped = errors.New("statechart: actor stopped") )
Functions ¶
This section is empty.
Types ¶
type Actor ¶
Actor is the runtime instance of a statechart Machine. One Actor is instantiated per workflow execution / unit test. It reads no clock and starts no goroutine, so it is safe to drive from a Temporal workflow goroutine.
func NewActor ¶
func NewActor[Ctx any, Evt any](m *Machine[Ctx, Evt], opts ...ActorOption) *Actor[Ctx, Evt]
NewActor constructs a fresh Actor in the Stopped status. Call Start to transition it to Running and observe the initial entry actions.
func NewActorFromSnapshot ¶
func NewActorFromSnapshot[Ctx any, Evt any](m *Machine[Ctx, Evt], persisted []byte) (*Actor[Ctx, Evt], error)
NewActorFromSnapshot constructs an actor seeded from a JSON snapshot.
Restoration sequence:
- Validates the snapshot version is supported.
- Validates the state value against the machine, returning ErrSnapshotMismatch when it names a state the machine does not have, gives a compound state more than one active child, or leaves a parallel region out.
- Rebuilds the history memory by resolving stored path strings to stateNode pointers within the supplied machine.
- Restores any queued internal events.
The restored actor has the same status as when persisted; if it was running, it is running after restoration (no Start needed).
func (*Actor[Ctx, Evt]) Can ¶
Can reports whether evt would be handled by the current configuration: that is, whether at least one transition selects for it once guards are evaluated against the current context. It does not mutate the actor.
Send deliberately drops an unhandled event, because in a statechart an event no state cares about is not an error. Can is the companion for callers that do treat it as one, and for which "the machine ignored that" must be distinguishable from "the machine acted on it":
if !actor.Can(evt) {
return fmt.Errorf("%w: %s in %s", ErrUnhandledEvent, name, snap.Value.Path())
}
_ = actor.Send(ctx, evt)
Because guards are pure by contract, the answer is exact rather than an approximation, and asking costs nothing beyond the guard evaluations. Two boundaries are worth knowing. An actor that is not running reports false for every event, since a stopped or completed actor handles none. And Can answers about transition *selection*: a selected transition whose target cannot be resolved reports true here while changing no state, which is the same configuration error Send absorbs.
func (*Actor[Ctx, Evt]) Enabled ¶ added in v0.7.0
Enabled returns the names from Actor.NextEvents whose event would fire a transition now, guards evaluated, sorted. byName builds the event for a name and reports false for a name it does not know, which leaves that name out.
A guard that reads the event's payload sees the payload byName supplies, so the answer is exact only for the events byName builds.
func (*Actor[Ctx, Evt]) FireTimer ¶
FireTimer fires the armed delayed transition with the given id. It is the write half of the pull-based timer interface (see Actor.PendingTimers) and is how an adapter delivers an elapsed "after" delay back to the machine. Firing an id that is not currently armed, or firing when the owning state is no longer active, is a safe no-op.
The reported bool is true when the timer was armed and its owning state was still active, so the delay reached the machine. A false result means the id was unknown, already fired, or cancelled by an exit. Adapters that reconcile timers against an external clock use it to tell a delivered delay from a late callback for a state the machine has already left; callers that do not care may discard it.
func (*Actor[Ctx, Evt]) NextEvents ¶ added in v0.7.0
NextEvents returns the names of the events the active configuration declares a transition for, sorted. It reads every active state and its ancestors, the same handlers Send would consult, and leaves out the "*" wildcard.
Guards are not evaluated, because a guard needs an event value and a name is not one. Actor.Enabled lists only the events that would fire now.
An actor that is not running reports none.
Example ¶
ExampleActor_NextEvents lists the events a task accepts in its current state and previews one of them without sending it.
package main
import (
"context"
"fmt"
"github.com/arisros/fate/engine"
)
func main() {
type Ctx struct{ Score int }
type state = engine.StateNodeConfig[Ctx, string]
type transition = engine.TransitionConfig[Ctx, string]
m, err := engine.CreateMachine(engine.MachineConfig[Ctx, string]{
ID: "review",
Initial: "open",
Context: Ctx{Score: 40},
States: map[string]state{
"open": {On: map[string][]transition{
"APPROVE": {{Target: "approved", Guard: func(c Ctx, _ string) bool { return c.Score >= 60 }}},
"REJECT": {{Target: "rejected"}},
}},
"approved": {Type: engine.NodeFinal},
"rejected": {Type: engine.NodeFinal},
},
})
if err != nil {
panic(err)
}
a := engine.NewActor(m)
_ = a.Start(context.Background())
fmt.Println(a.NextEvents())
fmt.Println(a.Enabled(func(name string) (string, bool) { return name, true }))
next, _ := a.Preview("REJECT")
fmt.Println(next.Value.Path(), next.Status)
fmt.Println(a.Snapshot().Value.Path())
}
Output: [APPROVE REJECT] [REJECT] rejected done open
func (*Actor[Ctx, Evt]) PendingInvocations ¶
func (a *Actor[Ctx, Evt]) PendingInvocations() []effect.PendingInvocation
PendingInvocations returns the actor's currently-armed invocations, in deterministic order (by ID). It is the read half of the invoke effect: an adapter runs each Src and reports the outcome via ResolveInvocation / RejectInvocation. The core never runs an invocation itself. See ADR-0004.
func (*Actor[Ctx, Evt]) PendingTimers ¶
func (a *Actor[Ctx, Evt]) PendingTimers() []effect.PendingTimer
PendingTimers returns the actor's currently-armed delayed transitions, in deterministic order (by TimerID). It is the read half of the timer interface: an adapter arms its own durable or wall-clock timers from this list and calls Actor.FireTimer when a delay elapses. The core never fires a timer itself, so without an adapter pending timers simply remain armed. See ADR-0003.
func (*Actor[Ctx, Evt]) Persist ¶
Persist returns a JSON snapshot of the actor's state suitable for storage (e.g. ArangoDB) and later restoration via NewActorFromSnapshot.
Round-trip guarantee: NewActorFromSnapshot(m, actor.Persist()) produces an actor that, given the same future events, yields byte-identical Persist output to the original.
func (*Actor[Ctx, Evt]) Preview ¶ added in v0.7.0
Preview returns the snapshot Actor.Send would leave behind for evt, without changing the actor. Compare its Value with the current snapshot's to see where the event leads, or pass both to diff.Snapshots.
The event runs on a copy of the actor. When the machine sets MachineConfig.CloneContext the copy's context comes from it. Otherwise the copy is restored from Actor.Persist, so Preview fails where Persist does and a value held in an any comes back as its JSON form (a time.Time as a string, an int as a float64), which a guard that type-asserts will not match.
An event no transition handles yields the current snapshot unchanged; Actor.Can tells that apart from a transition that keeps the same state. Preview returns the error Send would: ErrActorStopped for an actor that is not running.
func (*Actor[Ctx, Evt]) RejectInvocation ¶
RejectInvocation reports failure of the invocation with the given id. If it is still armed and declares OnError, the mapped event is processed as an internal step. Rejecting an unknown or already-settled id is a safe no-op.
The reported bool carries the same meaning as in Actor.ResolveInvocation: true when the invocation was accepted, false when the id was unknown, already settled, or owned by a state the machine has since left. An accepted invocation with no OnError mapper reports true and delivers no event, which is how a failure with no declared handler is silently absorbed.
func (*Actor[Ctx, Evt]) ResolveInvocation ¶
ResolveInvocation reports successful completion of the invocation with the given id. If it is still armed (its state still active) and declares OnDone, the mapped event is processed as an internal step. Resolving an unknown or already-settled id is a safe no-op. The reported bool is true when the invocation was still armed and its owning state still active, so the outcome was accepted. It is false when the id was unknown, already settled, or belongs to a state the machine has since left. Note that true means accepted, not that an event was delivered: an accepted invocation with no OnDone mapper settles without producing one. Adapters use this to tell a delivered result from a late one; callers that do not care may discard it.
func (*Actor[Ctx, Evt]) Send ¶
Send dispatches an event to the actor synchronously. Returns after the event (and any events the transition raised internally) have been processed. Events that no transition handles are silently dropped.
If processing the event causes the actor to reach a top-level final state, its status transitions to StatusDone. Subsequent Sends are silently dropped (matching XState v5 semantics).
func (*Actor[Ctx, Evt]) Snapshot ¶
Snapshot returns the actor's current state. Safe to call concurrently.
func (*Actor[Ctx, Evt]) Start ¶
Start moves the actor into Running and executes entry actions for the initial configuration chain (deepest entry's Entry runs last). Idempotent. If the initial configuration already lands in a top-level final state, the actor immediately transitions to StatusDone.
func (*Actor[Ctx, Evt]) Stop ¶
func (a *Actor[Ctx, Evt]) Stop()
Stop terminates the actor and cancels any pending delayed transitions; subsequent Send returns ErrActorStopped.
func (*Actor[Ctx, Evt]) Subscribe ¶
Subscribe registers an observer that is called with a snapshot after every Send (and once on Start, after entry actions). Returns an unsubscribe func.
func (*Actor[Ctx, Evt]) SubscribeSteps ¶ added in v0.8.0
SubscribeSteps registers an observer that receives every Step, in order, before the snapshot observers of Actor.Subscribe run. An event that fires no transition produces no step. Returns an unsubscribe func.
The observer runs while the actor is locked, so it must not call the actor; Step.Value carries the configuration it would otherwise read.
Example ¶
ExampleActor_SubscribeSteps records what each step of an actor did: which transition fired and which states were left and entered.
package main
import (
"context"
"fmt"
"github.com/arisros/fate/engine"
)
func main() {
type state = engine.StateNodeConfig[struct{}, string]
type transition = engine.TransitionConfig[struct{}, string]
m, err := engine.CreateMachine(engine.MachineConfig[struct{}, string]{
ID: "task",
Initial: "draft",
States: map[string]state{
"draft": {On: map[string][]transition{"SUBMIT": {{Target: "review"}}}},
"review": {On: map[string][]transition{"RETURN": {{Target: "review"}}}},
},
})
if err != nil {
panic(err)
}
a := engine.NewActor(m)
a.SubscribeSteps(func(s engine.Step) {
fmt.Println(s.Seq, s.Cause, s.Event, s.Exited, s.Entered)
})
_ = a.Start(context.Background())
_ = a.Send(context.Background(), "SUBMIT")
_ = a.Send(context.Background(), "RETURN")
}
Output: 1 start [] [draft] 2 event SUBMIT [draft] [review] 3 event RETURN [review] [review]
type ActorOption ¶
type ActorOption func(*actorOpts)
ActorOption configures a new Actor.
func WithInitialValue ¶
func WithInitialValue[Ctx any, Evt any](v persist.StateValue) ActorOption
WithInitialValue overrides the actor's starting state. Used by NewActorFromSnapshot (P6) and by tests that need to seed mid-flight. The value must be a valid configuration of the machine; this is not re-validated in the skeleton.
func WithLogger ¶
func WithLogger(fn func(string)) ActorOption
WithLogger sets the function called by Log actions and internal warnings. Default: a no-op (logs are discarded).
type Finding ¶ added in v0.10.0
type Finding struct {
Kind FindingKind `json:"kind"`
// State is the dot path of the state the finding is about.
State string `json:"state"`
Message string `json:"message"`
}
Finding is one problem Lint found in a machine.
type FindingKind ¶ added in v0.10.0
type FindingKind string
FindingKind names what Lint found.
const ( // FindingUnreachable is a state no initial chain or transition enters. FindingUnreachable FindingKind = "unreachable" // FindingDeadEnd is a state that is not final and that no transition, on // it or on an ancestor, leaves. FindingDeadEnd FindingKind = "dead_end" // FindingOnDoneNeverFires is a state that declares OnDone and can never // complete: a compound state with no final child, or a parallel state with // a region that cannot complete. FindingOnDoneNeverFires FindingKind = "on_done_never_fires" )
The kinds of Finding that Lint reports.
type History ¶
type History uint8
History selects the depth of memory for a NodeHistory pseudo-state.
- HistoryShallow remembers only the immediate child of the parent compound. On re-entry, the parent restarts that child via the child's initial chain.
- HistoryDeep remembers the full descendant configuration. On re-entry, the entire active sub-tree at exit time is restored.
type Machine ¶
Machine is an immutable, validated statechart. Safe to share across goroutines and across multiple Actor instances. Construct via CreateMachine; never mutate.
func CreateMachine ¶
func CreateMachine[Ctx any, Evt any](cfg MachineConfig[Ctx, Evt]) (*Machine[Ctx, Evt], error)
CreateMachine validates a MachineConfig and returns an immutable *Machine. Returns ErrInvalidConfig (with a descriptive wrapped error) for malformed configurations.
func (*Machine[Ctx, Evt]) Describe ¶
func (m *Machine[Ctx, Evt]) Describe() describe.MachineDescriptor
Describe returns a MachineDescriptor for the machine. The context is JSON-marshaled if possible; on marshal failure (e.g. a Ctx containing a channel) the Context field is left nil and the rest of the descriptor still renders correctly.
Action names come from each value's ImplName() method: the built-in actions report their kind ("assign", "raise:CANCEL", "log"), and action.Named attaches a caller-chosen label. Guard names come from TransitionConfig.GuardName, since a func value carries no name a descriptor could recover. Anything unnamed falls back to "".
func (*Machine[Ctx, Evt]) IsKnownState ¶
IsKnownState reports whether `name` is a valid state name anywhere in the machine. The check is recursive — it matches both top-level states and nested children. This mirrors the legacy fp.StateMachine.AsStateValidator behavior used by LPW.
func (*Machine[Ctx, Evt]) IsLegalTransition ¶
IsLegalTransition reports whether `eventName` declared on state `from` (or any of its ancestors, mirroring transition bubbling at runtime) has at least one candidate transition. It does NOT evaluate guards — guards require an event payload and context, neither of which are available here.
Use this when you want stricter-than-set-membership validation. The LPW port keeps the legacy set-membership default (via IsKnownState) for backward compat per migration-playbook P12 decision; opt into IsLegalTransition where stricter checks are wanted.
func (*Machine[Ctx, Evt]) IsTerminal ¶
IsTerminal reports whether `name` is a state with Type == NodeFinal. Replaces the legacy IsTerminalStatus consumer in termination.go.
func (*Machine[Ctx, Evt]) Lint ¶ added in v0.10.0
Lint reports states that are legal but probably mistakes: unreachable states, dead ends, and OnDone transitions that can never fire. CreateMachine accepts all of them, so call Lint from a test to keep a machine clean.
The analysis reads structure only. Guards are assumed able to pass, so a state behind a guard that is never true is still counted as reachable. Findings are sorted by state path, then kind; a clean machine returns nil.
Example ¶
ExampleMachine_Lint finds a state nothing enters and a state nothing leaves.
package main
import (
"fmt"
"github.com/arisros/fate/engine"
)
func main() {
type state = engine.StateNodeConfig[struct{}, string]
type transition = engine.TransitionConfig[struct{}, string]
m, err := engine.CreateMachine(engine.MachineConfig[struct{}, string]{
ID: "task",
Initial: "draft",
States: map[string]state{
"draft": {On: map[string][]transition{"SUBMIT": {{Target: "review"}}}},
"review": {},
"returned": {On: map[string][]transition{"SUBMIT": {{Target: "review"}}}},
},
})
if err != nil {
panic(err)
}
for _, f := range m.Lint() {
fmt.Println(f.State, f.Kind)
}
}
Output: returned unreachable review dead_end
func (*Machine[Ctx, Evt]) States ¶
States returns the names of every state in the machine (top-level + nested) in deterministic order: top-down, alphabetical within siblings. Used by schema-vs-FSM enum sync checks (LPW expects status enum to match machine states exactly).
func (*Machine[Ctx, Evt]) UIState ¶
func (m *Machine[Ctx, Evt]) UIState(v persist.StateValue, ctx Ctx) (map[string]json.RawMessage, error)
UIState evaluates the view models of the active configuration v against ctx, keyed by the dot path of the state that declares each one.
For each active leaf, the nearest state on its path (the leaf itself or an ancestor) that declares a UIState contributes, once even when several leaves share it. The result is nil when no active state declares one. A view model that fails to marshal, or whose function panics, returns an error naming the state.
type MachineConfig ¶
type MachineConfig[Ctx any, Evt any] struct { // ID is a human-readable identifier used in inspection output and as the // stable prefix for spawn IDs (per ADR-002). ID string // Initial is the starting child state name. Required. Initial string // Context is the seed value for the actor's running context. Context Ctx // CloneContext, if set, returns a copy of a context that shares no mutable // state with the original. Set it when Ctx holds a map, slice or pointer. // NewActor uses it so actors of one machine do not share the seed, and // Actor.Preview uses it in place of a JSON round trip, which keeps the Go // types of values held in an any (a time.Time stays a time.Time). CloneContext func(Ctx) Ctx // States is the map of immediate child state nodes. Keys are local state // names (e.g. "idle"); values describe each node. States map[string]StateNodeConfig[Ctx, Evt] }
MachineConfig declares an immutable statechart. Pass to CreateMachine to validate and obtain a *Machine.
Generics: Ctx is the user's context (data accumulated as the machine runs); Evt is the user's event type (typically a sealed interface).
type NodeType ¶
type NodeType uint8
NodeType discriminates state node kinds. As of P5, Atomic, Compound, Final, and History are supported; Parallel is the remaining P5 piece.
type SelectedTransition ¶
type SelectedTransition[Ctx any, Evt any] struct { Source *stateNode[Ctx, Evt] Config TransitionConfig[Ctx, Evt] }
SelectedTransition records the outcome of selectTransitions per active leaf: the resolved source node and the matching transition config.
type Setup ¶
Setup is a type-safe registry of named guards and actions, mirroring XState v5's setup({ guards, actions }) ergonomic. Register implementations once, then reference them by name while declaring a MachineConfig via the Setup.Guard and Setup.Action accessors. This keeps large machine configs readable and lets several transitions share one implementation.
Setup is sugar over CreateMachine; it adds no semantics the declarative config cannot express. A typical use:
s := engine.NewSetup[Ctx, Evt]().
WithGuard("isHighRisk", func(c Ctx, _ Evt) bool { return c.Risk == "HIGH" }).
WithAction("clearForm", action.Assign(func(c Ctx, _ Evt) Ctx { c.Form = nil; return c }))
m, err := s.CreateMachine(engine.MachineConfig[Ctx, Evt]{
ID: "review", Initial: "open",
States: map[string]engine.StateNodeConfig[Ctx, Evt]{
"open": {On: map[string][]engine.TransitionConfig[Ctx, Evt]{
"NEXT": {{Target: "closed", Guard: s.Guard("isHighRisk"),
Actions: []action.Action[Ctx, Evt]{s.Action("clearForm")}}},
}},
"closed": {Type: engine.NodeFinal},
},
})
Referencing a name that was never registered is reported as an error from Setup.CreateMachine, so typos surface at construction time rather than silently doing nothing.
func NewSetup ¶
NewSetup returns an empty registry. Register entries with Setup.WithGuard and Setup.WithAction (both chainable).
func (*Setup[Ctx, Evt]) Action ¶
Action returns the action registered under name for use in a TransitionConfig or a state's Entry/Exit. If no action is registered under name, Action records the missing reference (so Setup.CreateMachine returns an error) and returns a no-op action.
The returned action carries name, so it appears under that name in a describe.MachineDescriptor and in every rendered diagram, without the caller repeating it through action.Named. Wrapping does not change how the action runs.
func (*Setup[Ctx, Evt]) CreateMachine ¶
func (s *Setup[Ctx, Evt]) CreateMachine(cfg MachineConfig[Ctx, Evt]) (*Machine[Ctx, Evt], error)
CreateMachine validates and builds the machine, first reporting any guard or action names referenced via Setup.Guard / Setup.Action that were never registered. On success it is identical to calling CreateMachine directly.
func (*Setup[Ctx, Evt]) Guard ¶
Guard returns the guard registered under name for use in a TransitionConfig. If no guard is registered under name, Guard records the missing reference (so Setup.CreateMachine returns an error) and returns a guard that never passes, keeping config construction safe to continue.
func (*Setup[Ctx, Evt]) WithAction ¶
WithAction registers an action under name and returns the Setup for chaining. Registering the same name twice replaces the earlier action.
type StateNodeConfig ¶
type StateNodeConfig[Ctx any, Evt any] struct { // Type is the node kind. If zero, it is inferred: NodeAtomic when States // is empty; NodeCompound otherwise. Type NodeType // Initial is the starting child state name. Required when Type is // NodeCompound and States is non-empty. Initial string // States declares immediate child state nodes (compound nesting). States map[string]StateNodeConfig[Ctx, Evt] // On maps event names to ordered transition candidates. The first // candidate whose guard passes (or has no guard) is selected. On map[string][]TransitionConfig[Ctx, Evt] // After declares delayed transitions, keyed by delay. When this state is // entered the actor records one pending timer per delay; exiting the state // disarms them. The core never fires a timer itself (it is clock-agnostic): // an adapter discovers armed timers via Actor.PendingTimers and delivers an // elapsed delay via Actor.FireTimer, at which point the first transition in // that delay's slice whose Guard and Cond pass fires (as an internal step // with the zero Evt). Mirrors XState's `after`. See ADR-0003. After map[time.Duration][]TransitionConfig[Ctx, Evt] // Invoke declares external work run while this state is active (XState's // invoke). On entry each invocation is recorded as pending; on exit it is // disarmed. The core never executes an invocation — an adapter pulls them // via Actor.PendingInvocations and reports outcomes via // Actor.ResolveInvocation / Actor.RejectInvocation. See ADR-0004. Invoke []effect.Invocation[Ctx, Evt] // Entry actions run, in declaration order, when this state is entered. // For a compound node, Entry runs before the child's Entry. Entry []action.Action[Ctx, Evt] // Exit actions run, in declaration order, when this state is exited. // For a compound node, Exit runs after the child's Exit (deepest first). Exit []action.Action[Ctx, Evt] // OnDone declares transitions to fire when this node completes: a compound // node when its active child reaches a final state, a parallel node when // every region has. Empty for atomic / final nodes. OnDone []TransitionConfig[Ctx, Evt] // History selects HistoryShallow or HistoryDeep when Type is NodeHistory. // Ignored for other node types. History History // Default is the fallback target for a NodeHistory pseudo-state when // no prior memory exists. Optional; if empty, the parent compound's // initial child is used. Default string // Output, set only on a NodeFinal state, builds the machine's output value // from the final context when a top-level final state is reached. The // result is JSON-marshaled into the snapshot's Output field. Mirrors // XState's final-state output. Output func(ctx Ctx) any // Meta is data for tooling and hosts: a form name, a task type, a display // order. The engine never reads it. CreateMachine encodes it as JSON, so // values must be JSON-marshalable, and Describe publishes it as "meta". Meta map[string]any // UIState projects the context into a view model while this state is // active. Build it with UIStateOf. See Machine.UIState. UIState *describe.UIState[Ctx] }
StateNodeConfig declares one state node within a machine. State nodes nest via the States field to form compound hierarchies.
type Step ¶ added in v0.8.0
type Step struct {
// Seq numbers the steps of an actor from 1. It is stored in the snapshot,
// so it keeps counting after a restore.
Seq uint64 `json:"seq"`
Cause StepCause `json:"cause"`
// Event is the event's name for StepEvent, StepRaise and StepInvoke.
Event string `json:"event,omitempty"`
// Effect is the timer or invocation id for StepTimer and StepInvoke.
Effect string `json:"effect,omitempty"`
Transitions []StepTransition `json:"transitions,omitempty"`
Exited []string `json:"exited,omitempty"`
Entered []string `json:"entered,omitempty"`
// Value is the active configuration after the step.
Value persist.StateValue `json:"value"`
}
Step records what one step of an actor did. A single Send produces one step for the event and one more for each event its actions raised and each OnDone that followed, in the order they ran.
Exited and Entered list state paths in the order their exit and entry actions ran. A state that a step leaves and enters again appears in both, which is how a host tells a restarted state from one that stayed active: its timers and invocations keep the same ids across the re-entry.
type StepCause ¶ added in v0.8.0
type StepCause string
StepCause says what set a step off.
const ( // StepStart is Actor.Start entering the initial configuration. StepStart StepCause = "start" // StepEvent is an event passed to Actor.Send. StepEvent StepCause = "event" // StepRaise is an event an action raised, taken from the internal queue. StepRaise StepCause = "raise" // StepDone is an OnDone transition fired by a completed state. StepDone StepCause = "done" // StepTimer is a delayed transition delivered by Actor.FireTimer. StepTimer StepCause = "timer" // StepInvoke is the event an invocation outcome mapped to. StepInvoke StepCause = "invoke" )
The causes a Step can have.
type StepTransition ¶ added in v0.8.0
type StepTransition struct {
Source string `json:"source"`
Target string `json:"target,omitempty"`
Internal bool `json:"internal,omitempty"`
}
StepTransition is one transition that fired in a step. Target is empty for a transition that only runs actions, and names the state actually entered when the declared target was a history state.
type TransitionConfig ¶
type TransitionConfig[Ctx any, Evt any] struct { // Target is the destination state, named by its local name (sibling) // or by a dot-separated descendant path (e.g. "parent.child"). // An empty Target means the transition is internal (no state change). Target string // Internal, when true, suppresses exit/re-entry of the source state for // targets that are descendants of the source (matches XState's // `internal: true`). Default false (external transition). Internal bool // Guard, if non-nil, must return true for the transition to be selected. // Otherwise the next candidate in the slice is tried, then ancestors are // consulted. A Guard is a pure predicate over context and event. Guard action.Guard[Ctx, Evt] // GuardName labels Guard in a [describe.MachineDescriptor], and through it in every // rendered diagram. Guard is a func value with no identity a descriptor can // recover, so a guard is unnamed unless it is named here. Optional; an // unnamed guard renders as "". GuardName string // Cond, if non-nil, is a structural condition over the active state // configuration (see Cond / StateIn / InState). When both Guard and Cond // are set, the transition is selected only if both pass. Use Cond for // "in state X" checks that a context/event Guard cannot express. Cond action.Cond // Actions run after exit actions and before entry actions when the // transition fires. Order: declaration order. Actions []action.Action[Ctx, Evt] // Meta is data for tooling and hosts about this transition: a button // title, an order, a hidden flag. The engine never reads it. It follows the // rules of StateNodeConfig.Meta and is accepted on On and OnDone // transitions. Meta map[string]any // CondMeta documents the context fields Guard checks, for tooling only. // It does not change whether the transition fires. Build it with Gates. CondMeta *action.CondMeta // contains filtered or unexported fields }
TransitionConfig declares one possible transition for an event.