Documentation
¶
Overview ¶
Package fate is a statechart engine for Go.
fate implements Harel statecharts: hierarchical (nested) states, parallel regions, and deep/shallow history — not a flat finite automaton. It is inspired by the semantics of SCXML and XState v5, expressed idiomatically in Go with strong typing via generics over a user-defined context (Ctx) and event (Evt) type.
Why "statechart", not "state machine" ¶
A classic finite-state machine has one active state at a time and no nesting. A statechart adds hierarchy (a state can contain sub-states), orthogonality (independent parallel regions that are all active at once), and history (re-entering a compound state can restore the sub-state it was last in). These features collapse the combinatorial state explosion that makes flat machines unmanageable for real workflows. fate is a statechart engine; the name is not an acronym.
Design principles ¶
- Zero dependencies: the root module imports only the standard library. Anything that needs an external dependency (the Temporal integration) lives in a separate module so adopters opt in explicitly.
- Determinism: a Machine is immutable once constructed and is safe to share across goroutines. All observable iteration is ordered. Given the same machine and the same event sequence, an Actor produces a byte-identical persisted snapshot. This makes fate safe to drive from deterministic execution environments such as Temporal workflows.
- Persistence first: actor state serialises to and restores from JSON via Actor.Persist and NewActorFromSnapshot. The snapshot shape is versioned so it can evolve without breaking stored data.
Two ways to define a machine ¶
Define a machine directly with CreateMachine and the declarative MachineConfig / StateNodeConfig / TransitionConfig structs, or use the type-safe Setup builder to register named guards, actions and actors once and reference them by name from the config. Both produce the same immutable Machine.
Driving a machine ¶
Construct an Actor from a Machine, Actor.Start it, and feed it events with Actor.Send. Read the current state with Actor.Snapshot, observe changes with Actor.Subscribe, and persist/restore with Actor.Persist and NewActorFromSnapshot. To drive a machine inside a Temporal workflow, use the WorkflowActor from the github.com/arisros/fate/temporal module instead of a bare Actor.
See the examples directory and the package examples for runnable machines.
Example ¶
Example builds a one-state counter machine, drives it with two events, and reads the accumulated context.
package main
import (
"context"
"fmt"
fate "github.com/arisros/fate"
)
func main() {
type Ctx struct{ Count int }
m, err := fate.CreateMachine(fate.MachineConfig[Ctx, string]{
ID: "counter",
Initial: "active",
States: map[string]fate.StateNodeConfig[Ctx, string]{
"active": {On: map[string][]fate.TransitionConfig[Ctx, string]{
"INC": {{Actions: []fate.Action[Ctx, string]{
fate.Assign(func(c Ctx, _ string) Ctx { c.Count++; return c }),
}}},
}},
},
})
if err != nil {
panic(err)
}
a := fate.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"
fate "github.com/arisros/fate"
)
func main() {
m, _ := fate.CreateMachine(fate.MachineConfig[struct{}, string]{
ID: "blink",
Initial: "off",
States: map[string]fate.StateNodeConfig[struct{}, string]{
"off": {After: map[time.Duration][]fate.TransitionConfig[struct{}, string]{
time.Hour: {{Target: "on"}},
}},
"on": {Type: fate.NodeFinal},
},
})
a := fate.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 (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"
fate "github.com/arisros/fate"
)
func main() {
type Ctx struct{ Count int }
build := func() *fate.Machine[Ctx, string] {
m, _ := fate.CreateMachine(fate.MachineConfig[Ctx, string]{
ID: "counter",
Initial: "active",
States: map[string]fate.StateNodeConfig[Ctx, string]{
"active": {On: map[string][]fate.TransitionConfig[Ctx, string]{
"INC": {{Actions: []fate.Action[Ctx, string]{
fate.Assign(func(c Ctx, _ string) Ctx { c.Count++; return c }),
}}},
}},
},
})
return m
}
a := fate.NewActor(build())
_ = a.Start(context.Background())
_ = a.Send(context.Background(), "INC")
blob, _ := a.Persist()
restored, _ := fate.NewActorFromSnapshot[Ctx, string](build(), blob)
_ = restored.Send(context.Background(), "INC")
fmt.Println(restored.Snapshot().Context.Count)
}
Output: 2
Index ¶
- Constants
- Variables
- func RenderASCII(d MachineDescriptor, opts RenderOptions) string
- func RenderMermaid(d MachineDescriptor, opts MermaidOptions) string
- func RenderTransitions(d MachineDescriptor, path string) string
- type Action
- type Actor
- func (a *Actor[Ctx, Evt]) FireTimer(id TimerID)
- func (a *Actor[Ctx, Evt]) PendingInvocations() []PendingInvocation
- func (a *Actor[Ctx, Evt]) PendingTimers() []PendingTimer
- func (a *Actor[Ctx, Evt]) Persist() ([]byte, error)
- func (a *Actor[Ctx, Evt]) PersistDeterministic() ([]byte, error)
- func (a *Actor[Ctx, Evt]) RejectInvocation(id InvokeID, err error)
- func (a *Actor[Ctx, Evt]) ResolveInvocation(id InvokeID, output any)
- func (a *Actor[Ctx, Evt]) Send(_ context.Context, evt Evt) error
- func (a *Actor[Ctx, Evt]) Snapshot() 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(Snapshot[Ctx])) func()
- type ActorOption
- type ActorStatus
- type Cond
- type DiffEntry
- type DiffKind
- type Enqueuer
- type Graph
- type GraphEdge
- type GraphNode
- type Guard
- type History
- type Invocation
- type InvokeID
- type Machine
- func (m *Machine[Ctx, Evt]) 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]) States() []string
- type MachineConfig
- type MachineDescriptor
- type MermaidOptions
- type NodeType
- type PendingInvocation
- type PendingTimer
- type RenderOptions
- type SelectedTransition
- type Setup
- func (s *Setup[Ctx, Evt]) Action(name string) 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) Guard[Ctx, Evt]
- func (s *Setup[Ctx, Evt]) WithAction(name string, a Action[Ctx, Evt]) *Setup[Ctx, Evt]
- func (s *Setup[Ctx, Evt]) WithGuard(name string, g Guard[Ctx, Evt]) *Setup[Ctx, Evt]
- type Snapshot
- type SnapshotDiff
- type StateNodeConfig
- type StateNodeDescriptor
- type StateValue
- type TimerID
- type TransitionConfig
- type TransitionDescriptor
Examples ¶
Constants ¶
const SnapshotVersion = 1
SnapshotVersion is the on-disk shape version. Incremented for backward-incompatible changes per ADR-003.
const Version = "0.4.0"
Version is the semantic version of the fate library. It is updated by the release process and surfaced here so programs can report the engine version they were built against.
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") // 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 ¶
func RenderASCII ¶
func RenderASCII(d MachineDescriptor, opts RenderOptions) string
RenderASCII produces a multi-line ASCII rendering of the descriptor. The result is suitable for printing to a terminal, embedding in a fixed-width log, or feeding to the studio's machine_view buffer.
State child order is alphabetical for determinism (matches the rest of the library; see ADR-002 / ADR-007). Initial states are tagged with a trailing "(initial)" annotation.
func RenderMermaid ¶
func RenderMermaid(d MachineDescriptor, opts MermaidOptions) string
RenderMermaid produces a Mermaid `stateDiagram-v2` document for the machine.
Node IDs are the sanitised dot-path (so e.g. main.done and head_vd.done are distinct IDs `main_done` / `head_vd_done`, avoiding the collisions plain leaf names would cause). The human label keeps the leaf name. Compound and parallel states nest; parallel regions are divided by `--`.
func RenderTransitions ¶
func RenderTransitions(d MachineDescriptor, path string) string
RenderTransitions emits a sidebar block showing every transition out of the state at the given dot-path. Returns an empty string if the path is not found in the descriptor. Format:
<event> [guard:NAME]: → <target> {Internal} [actions: A1, A2]
Multiple alternatives for the same event appear on consecutive lines.
Types ¶
type Action ¶
Action is something executed as part of a transition or on state entry / exit. Actions may update the context, raise internal events, or log; I/O is forbidden (see ADR-002). All actions are pure with respect to time and randomness.
Action is an interface because we want polymorphic concrete types (assignAction, raiseAction, etc.) while keeping the API ergonomic.
func Assign ¶
Assign returns an action that replaces the context with the result of fn. The function must be pure with respect to time, randomness, and I/O.
Note: fn returns a whole new Ctx rather than patching in place. For struct contexts, the idiomatic pattern is `func(c Ctx, _ Evt) Ctx { c.Field = v; return c }` which leverages Go's value semantics.
func EnqueueActions ¶
EnqueueActions returns an action that runs fn against an Enqueuer. All context updates are applied; raises are deferred until the batch completes (then enqueued into the actor's internal queue in order).
type Actor ¶
Actor is the runtime instance of a statechart Machine. One Actor is instantiated per workflow execution / unit test. NOT safe for use inside Temporal workflows — use WorkflowActor (P6) for that.
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.
- 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]) 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.
func (*Actor[Ctx, Evt]) PendingInvocations ¶
func (a *Actor[Ctx, Evt]) PendingInvocations() []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() []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]) PersistDeterministic ¶
PersistDeterministic returns a JSON snapshot with sorted map keys, matching the ADR-007 determinism requirement: identical actor state must produce byte-identical Persist output across runs.
The standard library's json.Marshal already sorts struct fields; map keys in StateValue.MarshalJSON are sorted; the history map iteration uses sorted keys here. So Persist() and PersistDeterministic() currently produce the same bytes — but PersistDeterministic is the explicit guarantee surface that downstream code should call when byte-equality matters.
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.
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.
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.
type ActorOption ¶
type ActorOption func(*actorOpts)
ActorOption configures a new Actor.
func WithInitialValue ¶
func WithInitialValue[Ctx any, Evt any](v 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 ActorStatus ¶
type ActorStatus string
ActorStatus is the lifecycle phase of an Actor.
const ( StatusRunning ActorStatus = "running" StatusStopped ActorStatus = "stopped" StatusDone ActorStatus = "done" StatusError ActorStatus = "error" )
type Cond ¶
type Cond interface {
// contains filtered or unexported methods
}
Cond is a structural transition condition evaluated against the actor's active state configuration, independent of context and event data. It is the fate equivalent of XState's stateIn guard.
A Guard sees only (context, event); a Cond sees only which states are currently active. The two are complementary: set both [TransitionConfig.Guard] and [TransitionConfig.Cond] and the transition fires only when both pass.
Build a Cond with StateIn / InState and compose with CondNot, CondAllOf, and CondAnyOf. Conds hold no mutable state and no reference to any actor, so a Cond built once is safe to share across machines and goroutines.
func CondAllOf ¶
CondAllOf returns a Cond that holds only when every supplied condition holds. With no arguments it always holds.
func CondAnyOf ¶
CondAnyOf returns a Cond that holds when at least one supplied condition holds. With no arguments it never holds.
func InState ¶
InState returns a Cond that holds when the active configuration includes the given dot-separated state path. Matching uses StateValue.Matches, so a prefix such as "menu.settings" matches any deeper active leaf beneath it.
type DiffEntry ¶
type DiffEntry struct {
Kind DiffKind `json:"kind"`
Field string `json:"field,omitempty"` // dot-path; empty for top-level kinds
From string `json:"from"` // left-hand JSON-encoded value
To string `json:"to"` // right-hand JSON-encoded value
}
DiffEntry is a single line of difference between two snapshots.
type DiffKind ¶
type DiffKind string
DiffKind enumerates the categories of difference DiffSnapshots may surface. Kept narrow on purpose; richer typing (e.g. "field added with value X") belongs to the renderer.
const ( // DiffKindStateValue: the active state configuration changed. Examples: // - left "active.main.verif", right "active.main.asset_doc" // - left "running", right "done" DiffKindStateValue DiffKind = "state_value" // DiffKindContextField: a field in the marshaled context JSON changed. // Field is the dot-path of the leaf, From/To are the JSON-encoded // values (so the renderer can pretty-print or color them). DiffKindContextField DiffKind = "context_field" // DiffKindStatus: actor status differs (running / done / stopped). DiffKindStatus DiffKind = "status" // DiffKindContextShape: the contexts have structurally different // JSON shapes (e.g. different field sets at some nesting level). // Surfaced when a sub-tree on one side is an object and the other is // a primitive or null. Field is the dot-path. DiffKindContextShape DiffKind = "context_shape" )
type Enqueuer ¶
Enqueuer is the surface inside an EnqueueActions block. It batches a series of context updates, raises, and logs into one atomic application — the raised events accumulate but are not processed until the whole batch's context updates are committed.
func (*Enqueuer[Ctx, Evt]) Assign ¶
func (e *Enqueuer[Ctx, Evt]) Assign(fn func(c Ctx, evt Evt) Ctx)
Assign applies fn to the running context. Subsequent calls compose.
func (*Enqueuer[Ctx, Evt]) Context ¶
func (e *Enqueuer[Ctx, Evt]) Context() Ctx
Context returns the in-progress context value. Useful for reading mid-batch.
type Graph ¶
type Graph struct {
ID string `json:"id"`
Initial string `json:"initial"` // qualified id of the top-level initial
Nodes []GraphNode `json:"nodes"`
Edges []GraphEdge `json:"edges"`
}
Graph is the full resolved structure for one machine.
func RenderGraphJSON ¶
func RenderGraphJSON(d MachineDescriptor) Graph
RenderGraphJSON converts a MachineDescriptor into a resolved Graph.
type GraphEdge ¶
type GraphEdge struct {
ID string `json:"id"`
Source string `json:"source"`
Event string `json:"event"`
Target string `json:"target"`
Guard string `json:"guard,omitempty"`
Actions []string `json:"actions,omitempty"`
Internal bool `json:"internal,omitempty"`
}
GraphEdge is one transition. Source/Target are qualified node ids; Event is the triggering event (the studio anchors the edge to the source node's matching event row, Stately-style).
type GraphNode ¶
type GraphNode struct {
ID string `json:"id"` // qualified node id (nodeID of dot-path)
Label string `json:"label"` // leaf name (display)
Path string `json:"path"` // dot-path (for active-state matching)
Type string `json:"type"` // atomic|compound|parallel|final|history
Parent string `json:"parent"` // parent qualified id, "" if top level
Initial bool `json:"initial"` // is its parent's initial child
History string `json:"history,omitempty"`
Entry []string `json:"entry,omitempty"`
Exit []string `json:"exit,omitempty"`
}
GraphNode is one state in the graph. Hierarchy is expressed via Parent (the qualified id of the enclosing compound/parallel node, "" for top level).
type Guard ¶
Guard is a pure predicate over context and event. Returning true selects the transition; returning false skips it. Guards must be pure (no I/O, no time, no randomness) — see ADR-002.
func AlwaysTrue ¶
AlwaysTrue is the implicit guard for transitions that declare no Guard. Exposed for combinator chaining.
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 Invocation ¶
type Invocation[Ctx any, Evt any] struct { // ID is unique within its state; combined with the state path it forms the // invocation's stable [InvokeID]. ID string // Src is the opaque logical name of the work to run. Src string // Input, if non-nil, builds the invocation input from the context captured // when the state is entered. Exposed to the adapter via PendingInvocation. Input func(ctx Ctx) any // OnDone, if non-nil, maps a successful result to an event the machine then // processes. If nil, a successful resolution is dropped. OnDone func(output any) Evt // OnError, if non-nil, maps a failure to an event the machine then // processes. If nil, a failure is dropped. OnError func(err error) Evt }
Invocation declares external work a state runs while it is active — XState's invoke. The fate core treats Src as an opaque name and never executes it: on entering the state the core records a pending invocation; on exit it disarms it. An adapter discovers pending invocations via Actor.PendingInvocations, runs the work named by Src, and reports the outcome via Actor.ResolveInvocation or Actor.RejectInvocation. The core then maps the outcome to an event (OnDone / OnError) and processes it — but only if the owning state is still active.
Because Src is opaque, the same mechanism expresses both a service/activity call and a spawned child machine: the adapter decides what Src means (a Temporal activity, a child workflow, a nested actor). See ADR-0004.
type InvokeID ¶
type InvokeID string
InvokeID identifies one armed invocation instance for as long as its state is active. It is derived deterministically from the owning state's path and the invocation's local ID, so the same logical invocation has the same ID across runs and across persistence.
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() 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 and Guard names come from each value's ImplName() method when implemented, falling back to "" otherwise. Anonymous closures therefore show as empty strings — callers that care should name their actions (see actions.go for helpers like Named, Assign).
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.
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 // 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 MachineDescriptor ¶
type MachineDescriptor struct {
ID string `json:"id"`
Initial string `json:"initial"`
Context json.RawMessage `json:"context,omitempty"`
States map[string]StateNodeDescriptor `json:"states"`
}
MachineDescriptor is the root of the descriptor tree.
func LoadDescriptor ¶
func LoadDescriptor(data []byte) (MachineDescriptor, error)
LoadDescriptor unmarshals a MachineDescriptor from JSON. Used by the TUI studio's static-view mode (P7) to load a machine without compiling Go code: the workflow team can `go run ./cmd/dump-descriptors` against a service, save the JSON output, and inspect it elsewhere.
Validation is shape-only — non-empty ID, at least one state, every state's type is a recognized string. Round-trip with Describe() is the authoritative contract; the function is intentionally permissive about extra fields (forward compatibility for future descriptor versions).
type MermaidOptions ¶
type MermaidOptions struct {
// Highlight maps active dot-paths to a marker (only the keys are used).
// Each active leaf and its ancestor composites get the `active` class.
Highlight map[string]rune
// Direction is the Mermaid layout direction: "TB" (default), "LR", etc.
Direction string
}
MermaidOptions controls the emitted diagram. Zero value renders top-to-bottom with no highlight.
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 PendingInvocation ¶
type PendingInvocation struct {
// ID is the invocation's stable identifier.
ID InvokeID
// Src is the opaque work name declared on the Invocation.
Src string
// Input is the payload built from context at arm time (nil if no Input fn).
Input any
}
PendingInvocation is what an adapter reads from Actor.PendingInvocations to learn which work to run. ID is passed back to ResolveInvocation / RejectInvocation when the work settles.
type PendingTimer ¶
type PendingTimer struct {
// ID is the timer's stable identifier, passed back to [Actor.FireTimer].
ID TimerID
// Delay is the configured delay of the underlying after-transition. An
// adapter that resumes a persisted actor is responsible for tracking how
// much of the delay has already elapsed.
Delay time.Duration
}
PendingTimer describes one delayed ("after") transition the actor currently has armed. It is what an adapter reads from Actor.PendingTimers to learn which timers to drive; when the adapter decides a delay has elapsed it calls Actor.FireTimer with the ID.
The fate core is clock-agnostic: it never sleeps, reads the wall clock, or starts a goroutine for a timer. It only records that a state wants to fire "after Delay" and exposes that intent. How and when the timer actually fires is entirely the adapter's responsibility (a Temporal adapter maps it to workflow.NewTimer; an in-memory adapter maps it to the OS clock; a test drives it by hand).
type RenderOptions ¶
type RenderOptions struct {
// Highlight maps dot-paths to a marker rune. The first match (longest
// path) on each line determines the marker shown.
Highlight map[string]rune
// IndentStep is the per-level indent. Defaults to 2 spaces.
IndentStep int
// CompoundOpen / CompoundClose bracket compound state blocks. Defaults
// to "┌─" / "└─" (Unicode box drawing).
CompoundOpen string
CompoundClose string
}
RenderOptions controls cosmetic aspects of ASCII rendering. Zero value renders deterministically with no highlight.
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 := fate.NewSetup[Ctx, Evt]().
WithGuard("isHighRisk", func(c Ctx, _ Evt) bool { return c.Risk == "HIGH" }).
WithAction("clearForm", fate.Assign(func(c Ctx, _ Evt) Ctx { c.Form = nil; return c }))
m, err := s.CreateMachine(fate.MachineConfig[Ctx, Evt]{
ID: "review", Initial: "open",
States: map[string]fate.StateNodeConfig[Ctx, Evt]{
"open": {On: map[string][]fate.TransitionConfig[Ctx, Evt]{
"NEXT": {{Target: "closed", Guard: s.Guard("isHighRisk"),
Actions: []fate.Action[Ctx, Evt]{s.Action("clearForm")}}},
}},
"closed": {Type: fate.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.
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 Snapshot ¶
type Snapshot[Ctx any] struct { Version int `json:"version"` Value StateValue `json:"value"` Context Ctx `json:"context"` Status ActorStatus `json:"status"` Output json.RawMessage `json:"output,omitempty"` Error string `json:"error,omitempty"` }
Snapshot is an immutable view of an actor's state at one instant. Safe to marshal to JSON and persist (see ADR-003).
The P3 skeleton only populates Version, Value, Context, and Status. Output, Error, Children, Queue, History, and Timers land in later phases.
type SnapshotDiff ¶
type SnapshotDiff struct {
Entries []DiffEntry `json:"entries"`
}
SnapshotDiff is the result of comparing two snapshots.
func DiffSnapshots ¶
func DiffSnapshots[Ctx any](left, right Snapshot[Ctx]) SnapshotDiff
DiffSnapshots computes the structural diff between two Snapshots of the same context type. Order of arguments is left → right; produced entries are listed in a deterministic order suitable for golden-file testing.
Context comparison serializes both sides to JSON and walks the resulting trees. Non-marshalable contexts surface a single DiffKindContextShape entry naming the cause.
func (SnapshotDiff) Empty ¶
func (d SnapshotDiff) Empty() bool
Empty reports whether the snapshots are equivalent — no entries means the renderer should show "no differences" rather than an empty list.
func (SnapshotDiff) Strings ¶
func (d SnapshotDiff) Strings() []string
Strings returns each entry's String form, sorted for stable output. The studio's golden-file tests rely on this ordering.
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 []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[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[Ctx, Evt] // OnDone declares transitions to fire when this compound node's active // child reaches a final state. Only meaningful for Type=NodeCompound // (or NodeParallel in P5 follow-up). 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 }
StateNodeConfig declares one state node within a machine. State nodes nest via the States field to form compound hierarchies.
type StateNodeDescriptor ¶
type StateNodeDescriptor struct {
Type string `json:"type"` // "atomic" | "compound" | "parallel" | "final" | "history"
Initial string `json:"initial,omitempty"`
Default string `json:"default,omitempty"` // history default target
History string `json:"history,omitempty"` // "shallow" | "deep" (only for history nodes)
Entry []string `json:"entry,omitempty"` // action names
Exit []string `json:"exit,omitempty"` // action names
On map[string][]TransitionDescriptor `json:"on,omitempty"`
OnDone []TransitionDescriptor `json:"on_done,omitempty"`
States map[string]StateNodeDescriptor `json:"states,omitempty"`
}
StateNodeDescriptor is the descriptor for a single state node. Mirrors StateNodeConfig but with strings where the original held function values or generic actions.
type StateValue ¶
type StateValue struct {
Leaf string
Children map[string]StateValue
}
StateValue represents the current configuration of a running statechart.
For an atomic or final state, Leaf is the state's local name and Children is nil. For a compound state, Leaf is the empty string and Children has exactly one entry: the active child's name → its own StateValue. For a parallel state, Children may have multiple entries — one per region.
JSON marshaling collapses atomic states to a bare string and compound / parallel states to a {"name": child} object, matching the XState v5 snapshot shape (see ADR-003).
func AtomicValue ¶
func AtomicValue(name string) StateValue
AtomicValue constructs a StateValue for a leaf node.
func CompoundValue ¶
func CompoundValue(children map[string]StateValue) StateValue
CompoundValue constructs a StateValue for a compound or parallel node. The children map keys are immediate child state names; values are their (possibly nested) StateValues.
func (StateValue) IsAtomic ¶
func (v StateValue) IsAtomic() bool
IsAtomic reports whether the value represents a leaf state.
func (StateValue) MarshalJSON ¶
func (v StateValue) MarshalJSON() ([]byte, error)
MarshalJSON implements json.Marshaler.
- Atomic state → "name"
- Compound/parallel → {"name": <child JSON>, ...}
func (StateValue) Matches ¶
func (v StateValue) Matches(target string) bool
Matches reports whether the state value matches the given dot-separated target path. A target "a.b" matches a value that is in state "a" with active descendant "b". A target "a" matches any value where region "a" is active (used for parallel-region queries).
func (StateValue) Path ¶
func (v StateValue) Path() string
Path returns the state value flattened to a dot-separated path. For a compound state {"a": {"b": "c"}}, it returns "a.b.c". For a parallel state with multiple regions, the regions are joined alphabetically with " | ": {"a": "x", "b": "y"} → "a.x | b.y". Used for human-friendly logging and inspection.
func (*StateValue) UnmarshalJSON ¶
func (v *StateValue) UnmarshalJSON(data []byte) error
UnmarshalJSON implements json.Unmarshaler. Accepts both the atomic form (a JSON string) and the compound/parallel form (a JSON object).
type TimerID ¶
type TimerID string
TimerID uniquely identifies one armed delayed ("after") transition for as long as it is pending. It is derived deterministically from the owning state's path, the delay, and the delay's index within that state, so the same logical timer keeps the same ID across runs and across persistence — a prerequisite for replay-safe driving by an adapter.
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 Guard[Ctx, Evt] // 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 Cond // Actions run after exit actions and before entry actions when the // transition fires. Order: declaration order. Actions []Action[Ctx, Evt] }
TransitionConfig declares one possible transition for an event.
type TransitionDescriptor ¶
type TransitionDescriptor struct {
Target string `json:"target,omitempty"`
Internal bool `json:"internal,omitempty"`
Guard string `json:"guard,omitempty"`
Actions []string `json:"actions,omitempty"`
}
TransitionDescriptor is the descriptor for a single transition entry. Guards / actions appear as names only.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
fate
command
Command fate inspects statecharts from the command line.
|
Command fate inspects statecharts from the command line. |
|
examples
|
|
|
quickstart
command
Command quickstart is the README example: a counter machine driven by events, then persisted and restored.
|
Command quickstart is the README example: a counter machine driven by events, then persisted and restored. |
|
realtime-timer
command
Command realtime-timer shows how to drive a machine's delayed ("after") transitions with the OS clock in a standalone program.
|
Command realtime-timer shows how to drive a machine's delayed ("after") transitions with the OS clock in a standalone program. |
|
trafficlight
command
Command trafficlight is the canonical compound-state example: red → green → yellow → red, with a pedestrian "walk" sub-state inside red.
|
Command trafficlight is the canonical compound-state example: red → green → yellow → red, with a pedestrian "walk" sub-state inside red. |
|
Package internal provides building blocks not exported from the public statechart API.
|
Package internal provides building blocks not exported from the public statechart API. |
|
temporal
module
|
|
|
Package testing provides helpers for asserting on Actor behavior in unit tests.
|
Package testing provides helpers for asserting on Actor behavior in unit tests. |