machine

package
v0.12.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 26, 2025 License: MIT Imports: 17 Imported by: 24

README

🦾 /pkg/machine

cd /

[!NOTE] asyncmachine-go is a batteries-included graph control flow library (AOP, actor model, state-machine).

/pkg/machine is a nondeterministic, multi-state, clock-based, relational, optionally accepting, and non-blocking state machine. It's a form of a rules engine that can orchestrate blocking APIs into fully controllable async state-machines. Write ops are state mutations, read ops are state checking, and subscriptions are state waiting.

Installation

import am "github.com/pancsta/asyncmachine-go/pkg/machine"

Features

Features are explained using Mermaid flow diagrams, and headers link to relevant sections of the manual.

Multi-state

Many states can be active at the same time.

Diagram showing multi-state capability
Clock and state contexts

States have clocks that produce contexts (odd = active; even = inactive).

Diagram showing state clocks and contexts
Queue

Queue of mutations enables lock-free Actor Model.

Diagram showing queue and mutations
AOP handlers

States are Aspects with Enter, State, Exit, and End handlers.

Diagram showing AOP handlers
Negotiation

Transitions are cancellable (during the negotiation phase).

Diagram showing negotiation phase
Relations

States are connected via Require, Remove, and Add relations.

Diagram showing state relations
Subscriptions

Channel-based broadcast for waiting on clock values.

Diagram showing subscriptions
Error handling

Error is a state, handled just like any other mutation.

val, err := someOp()
if err != nil {
    mach.AddErr(err, nil)
    return // no err needed
}
Tracers

Synchronous tracers for internal events.

TransitionInit TransitionStart TransitionEnd HandlerStart HandlerEnd
MachineInit MachineDispose NewSubmachine QueueEnd SchemaChange VerifyStates

Usage

Raw Strings
// ProcessingFile to FileProcessed
// 1 async and 1 sync state
package main

import am "github.com/pancsta/asyncmachine-go/pkg/machine"

func main() {
    // init the state machine
    mach := am.New(nil, am.Schema{
        "ProcessingFile": { // async
            Remove: am.S{"FileProcessed"},
        },
        "FileProcessed": { // async
            Remove: am.S{"ProcessingFile"},
        },
        "InProgress": { // sync
            Auto:    true,
            Require: am.S{"ProcessingFile"},
        },
    }, nil)
    mach.BindHandlers(&Handlers{
        Filename: "README.md",
    })
    // change the state
    mach.Add1("ProcessingFile", nil)
    // wait for completed
    select {
    case <-time.After(5 * time.Second):
        println("timeout")
    case <-mach.WhenErr(nil):
        println("err:", mach.Err())
    case <-mach.When1("FileProcessed", nil):
        println("done")
    }
}

type Handlers struct {
    Filename string
}

// negotiation handler
func (h *Handlers) ProcessingFileEnter(e *am.Event) bool {
    // read-only ops
    // decide if moving fwd is ok
    // no blocking
    // lock-free critical section
    return true
}

// final handler
func (h *Handlers) ProcessingFileState(e *am.Event) {
    // read & write ops
    // no blocking
    // lock-free critical section
    mach := e.Machine
    // clock-based expiration context
    stateCtx := mach.NewStateCtx("ProcessingFile")
    // unblock
    go func() {
        // re-check the state ctx
        if stateCtx.Err() != nil {
            return // expired
        }
        // blocking call
        err := processFile(h.Filename, stateCtx)
        if err != nil {
            mach.AddErr(err, nil)
            return
        }
        // re-check the state ctx after a blocking call
        if stateCtx.Err() != nil {
            return // expired
        }
        // move to the next state in the flow
        mach.Add1("FileProcessed", nil)
    }()
}
Waiting
// wait until FileDownloaded becomes active
<-mach.When1("FileDownloaded", nil)

// wait until FileDownloaded becomes inactive
<-mach.WhenNot1("DownloadingFile", nil)

// wait for EventConnected to be activated with an arg ID=123
<-mach.WhenArgs("EventConnected", am.A{"ID": 123}, nil)

// wait for Foo to have a tick >= 6
<-mach.WhenTime1("Foo", 6, nil)

// wait for Foo to have a tick >= 6 and Bar tick >= 10
<-mach.WhenTime(am.S{"Foo", "Bar"}, am.Time{6, 10}, nil)

// wait for DownloadingFile to have a tick increased by 2 since now
<-mach.WhenTicks("DownloadingFile", 2, nil)

// wait for an error
<-mach.WhenErr(nil)
Schema File
// BasicStatesDef contains all the states of the Basic state machine.
type BasicStatesDef struct {
    *am.StatesBase

    // ErrNetwork indicates a generic network error.
    ErrNetwork string
    // ErrHandlerTimeout indicates one of state machine handlers has timed out.
    ErrHandlerTimeout string

    // Start indicates the machine should be working. Removing start can force
    // stop the machine.
    Start string
    // Ready indicates the machine meets criteria to perform work, and requires
    // Start.
    Ready string
    // Healthcheck is a periodic request making sure that the machine is still
    // alive.
    Healthcheck string
}

var BasicSchema = am.Schema{

    // Errors

    ssB.ErrNetwork:        {Require: S{Exception}},
    ssB.ErrHandlerTimeout: {Require: S{Exception}},

    // Basics

    ssB.Start:       {},
    ssB.Ready:       {Require: S{ssB.Start}},
    ssB.Healthcheck: {},
}
Passing Args
// Example with typed state names (ssS) and typed arguments (A).
mach.Add1(ssS.KillingWorker, Pass(&A{
    ConnAddr:   ":5555",
    WorkerAddr: ":5556",
}))
Mutations and Relations

While mutations are the heartbeat of asyncmachine, it's the relations which define the rules of the flow. Check out the relations playground and quiz yourself (maybe a fancier playground).

mach := newMach("DryWaterWet", am.Schema{
    "Wet": {
        Require: am.S{"Water"},
    },
    "Dry": {
        Remove: am.S{"Water"},
    },
    "Water": {
        Add:    am.S{"Wet"},
        Remove: am.S{"Dry"},
    },
})
mach.Add1("Dry", nil)
mach.Add1("Water", nil)
// TODO quiz: is Wet active?

Demos

  • Relations playground
  • Interactively use the TUI debugger with data pre-generated by
    • libp2p-pubsub-simulator in
      • web terminal: http://188.166.101.108:8080/wetty/ssh
      • remote terminal: ssh 188.166.101.108 -p 4444
      • local terminal: go run github.com/pancsta/asyncmachine-go/tools/cmd/am-dbg@latest --import-data https://pancsta.github.io/assets/asyncmachine-go/am-dbg-exports/pubsub-sim.gob.br
    • remote integration tests in
      • web terminal: http://188.166.101.108:8081/wetty/ssh
      • remote terminal: ssh 188.166.101.108 -p 4445
      • local terminal: go run github.com/pancsta/asyncmachine-go/tools/cmd/am-dbg@latest --import-data https://pancsta.github.io/assets/asyncmachine-go/am-dbg-exports/remote-tests.gob.br

Examples

All examples and benchmarks can be found in /examples.

Tools

am-dbg

Apps

Documentation

API

The most common API methods are listed below. There's more for local state machines, but all of these are also implemented in the transparent RPC layer.

// TODO update
// A (arguments) is a map of named arguments for a Mutation.
type A map[string]any
// S (state names) is a string list of state names.
type S []string
type Time []uint64
type Clock map[string]uint64
type Result int
type Schema = map[string]State

// Api is a subset of Machine for alternative implementations.
type Api interface {
    // ///// REMOTE

    // Mutations (remote)

    Add1(state string, args A) Result
    Add(states S, args A) Result22
    Remove1(state string, args A) Result
    Remove(states S, args A) Result
    Set(states S, args A) Result
    AddErr(err error, args A) Result
    AddErrState(state string, err error, args A) Result

    // Traced mutations (remote)

    EvAdd1(event *Event, state string, args A) Result
    EvAdd(event *Event, states S, args A) Result
    EvRemove1(event *Event, state string, args A) Result
    EvRemove(event *Event, states S, args A) Result
    EvAddErr(event *Event, err error, args A) Result
    EvAddErrState(event *Event, state string, err error, args A) Result

    // Waiting (remote)

    WhenArgs(state string, args A, ctx context.Context) <-chan struct{}

    // Getters (remote)

    Err() error

    // ///// LOCAL

    // Checking (local)

    IsErr() bool
    Is(states S) bool
    Is1(state string) bool
    Not(states S) bool
    Not1(state string) bool
    Any(states ...S) bool
    Any1(state ...string) bool
    Has(states S) bool
    Has1(state string) bool
    IsTime(time Time, states S) bool
    IsClock(clock Clock) bool

    // Waiting (local)

    When(states S, ctx context.Context) <-chan struct{}
    When1(state string, ctx context.Context) <-chan struct{}
    WhenNot(states S, ctx context.Context) <-chan struct{}
    WhenNot1(state string, ctx context.Context) <-chan struct{}
    WhenTime(
        states S, times Time, ctx context.Context) <-chan struct{}
    WhenTime1(state string, tick uint64, ctx context.Context) <-chan struct{}
    WhenTicks(state string, ticks int, ctx context.Context) <-chan struct{}
    WhenErr(ctx context.Context) <-chan struct{}

    // Getters (local)

    StateNames() S
    ActiveStates() S
    Tick(state string) uint64
    Clock(states S) Clock
    Time(states S) Time
    TimeSum(states S) uint64
    NewStateCtx(state string) context.Context
    Export() *Serialized
    Schema() Schema
    Switch(groups ...S) string

    // Misc (local)

    Log(msg string, args ...any)
    Id() string
    ParentId() string
    Tags() []string
    SetLogId(val bool)
    GetLogId() bool
    SetLogger(logger Logger)
    SetLogLevel(lvl LogLevel)
    SetLoggerEmpty(lvl LogLevel)
    SetLoggerSimple(logf func(format string, args ...any), level LogLevel)
    Ctx() context.Context
    String() string
    StringAll() string
    Inspect(states S) string
    Index(state string) int
    BindHandlers(handlers any) error
    DetachHandlers(handlers any) error
    StatesVerified() bool
    Tracers() []Tracer
    DetachTracer(tracer Tracer) bool
    BindTracer(tracer Tracer) error
    Dispose()
    WhenDisposed() <-chan struct{}
    IsDisposed() bool
}

Tests

It's very easy to get a grasp of how asyncmachine works by reading the idiomatic test suite. Consider the example below of a method used to wait for certain arguments passing via a state activation:

func TestWhenArgs(t *testing.T) {
    // init
    m := NewRels(t, nil)

    // bind
    whenCh := m.WhenArgs("B", A{"foo": "bar"}, nil)

    // incorrect args
    m.Add1("B", A{"foo": "foo"})
    select {
    case <-whenCh:
        t.Fatal("whenCh shouldnt be selected")
    default:
        // pass
    }

    // correct args
    m.Add1("B", A{"foo": "bar"})
    select {
    case <-whenCh:
        // pass
    default:
        t.Fatal("whenCh should be selected")
    }

    // dispose
    m.Dispose()
    <-m.WhenDisposed()
}

Status

Release Candidate, semantically versioned, not optimized yet.

Concepts

asyncmachine is loosely based on the following concepts:

monorepo

Go back to the monorepo root to continue reading.

Documentation

Overview

Package machine is a nondeterministic, multi-state, clock-based, relational, optionally-accepting, and non-blocking state machine.

Index

Examples

Constants

View Source
const (
	// EnvAmDebug enables a simple debugging mode (eg long timeouts).
	// "2" logs to stdout (where applicable)
	// "1" | "2" | "" (default)
	EnvAmDebug = "AM_DEBUG"
	// EnvAmLog sets the log level.
	// "1" | "2" | "3" | "4" | "" (default)
	EnvAmLog = "AM_LOG"
	// EnvAmLogFile enables file logging (using machine ID as the name).
	// "1" | "" (default)
	EnvAmLogFile = "AM_LOG_FILE"
	// EnvAmDetectEval detects evals directly in handlers (use in tests).
	EnvAmDetectEval = "AM_DETECT_EVAL"
	// EnvAmTraceFilter will remove its contents from stack traces, shortening
	// them .
	EnvAmTraceFilter = "AM_TRACE_FILTER"
	// EnvAmTestDebug activates debugging in tests.
	EnvAmTestDebug = "AM_TEST_DEBUG"
	// HandlerGlobal is the name of a global transition handler.
	HandlerGlobal = "AnyEnter"
	// Any is a name of a meta-state used in catch-all handlers.
	Any         = "Any"
	SuffixEnter = "Enter"
	SuffixExit  = "Exit"
	SuffixState = "State"
	SuffixEnd   = "End"
	PrefixErr   = "Err"
)
View Source
const (
	// Exception is the name of the Exception state.
	Exception = "Exception"
)

Variables

View Source
var (
	// ErrStateUnknown indicates that the state is unknown.
	ErrStateUnknown = errors.New("state unknown")
	// ErrStateInactive indicates that a necessary state isn't active.
	ErrStateInactive = errors.New("state not active")
	// ErrCanceled indicates that a transition was canceled.
	ErrCanceled = errors.New("transition canceled")
	// ErrQueued indicates that a transition was queued.
	ErrQueued = errors.New("transition queued")
	// ErrInvalidArgs indicates that arguments are invalid.
	ErrInvalidArgs = errors.New("invalid arguments")
	// ErrHandlerTimeout indicates that a mutation timed out.
	ErrHandlerTimeout = errors.New("handler timeout")
	// ErrEvalTimeout indicates that an eval func timed out.
	ErrEvalTimeout = errors.New("eval timeout")
	// ErrTimeout indicates that a generic timeout occurred.
	ErrTimeout = errors.New("timeout")
	// ErrStateMissing indicates that states are missing.
	ErrStateMissing = errors.New("missing states")
	// ErrRelation indicates that a relation definition is invalid.
	ErrRelation = errors.New("relation error")
	// ErrDisposed indicates that the machine has been disposed.
	ErrDisposed = errors.New("machine disposed")
	// ErrSchema indicates an issue with the machine schema.
	ErrSchema = errors.New("schema error")
)
View Source
var CtxKey = &CtxKeyName{}
View Source
var LogArgs = []string{"name", "id", "port", "addr", "err"}

LogArgs is a list of common argument names to be logged. Useful for debugging.

View Source
var LogArgsMaxLen = 20

LogArgsMaxLen is the default maximum length of the arg's string representation.

Functions

func AMerge added in v0.9.0

func AMerge[K comparable, V any](maps ...map[K]V) map[K]V

AMerge merges 2 or more maps into 1. Useful for passing args from many packages.

func IsActiveTick added in v0.5.0

func IsActiveTick(tick uint64) bool

IsActiveTick returns true if the tick represents an active state (odd number).

func IsHandler added in v0.8.0

func IsHandler(states S, method string) (string, string)

IsHandler checks if a method name is a handler method, by returning a state name.

func IsTimeAfter

func IsTimeAfter(time1, time2 Time) bool

IsTimeAfter checks if time1 is after time2. Requires a deterministic states order, e.g. by using Machine.VerifyStates.

func ListHandlers added in v0.8.0

func ListHandlers(handlers any, states S) ([]string, error)

ListHandlers returns a list of handler method names from a handlers struct.

func MockClock added in v0.7.0

func MockClock(mach *Machine, clock Clock)

MockClock mocks the internal clock of the machine. Only for testing.

func NewArgsMapper added in v0.5.0

func NewArgsMapper(names []string, maxLen int) func(args A) map[string]string

NewArgsMapper returns a matcher function for LogArgs. Useful for debugging untyped argument maps.

maxLen: maximum length of the arg's string representation). Default to LogArgsMaxLen,

func NewStateGroups added in v0.8.0

func NewStateGroups[G any](groups G, mixins ...any) G

func NewStates added in v0.8.0

func NewStates[G States](states G) G

func StatesEqual added in v0.8.0

func StatesEqual(states1 S, states2 S) bool

func StatesToIndex added in v0.12.0

func StatesToIndex(index S, states S) []int

StatesToIndex returns a subset of [index] that matches [states]. Unknown states are represented by -1.

func TruncateStr added in v0.12.0

func TruncateStr(s string, maxLength int) string

TruncateStr with shorten the string and leave a tripedot suffix.

Types

type A

type A map[string]any

A (arguments) is a map of named arguments for a Mutation.

func Pass added in v0.8.0

func Pass(args *AT) A

Pass prepares A from AT, to pass to further mutations.

func PassMerge added in v0.8.0

func PassMerge(existing A, args *AT) A

PassMerge prepares A from AT and existing A, to pass to further mutations.

type AT added in v0.8.0

type AT struct {
	Err          error
	ErrTrace     string
	Panic        *ExceptionArgsPanic
	TargetStates S
	CalledStates S
	TimeBefore   Time
	TimeAfter    Time
	Event        *Event
}

AT represents typed arguments of pkg/machine, extracted from Event.Args via ParseArgs, or created manually to for Pass.

func ParseArgs added in v0.8.0

func ParseArgs(args A) *AT

ParseArgs extracts AT from A.

type Api added in v0.8.0

type Api interface {
	Add1(state string, args A) Result
	Add(states S, args A) Result
	Remove1(state string, args A) Result
	Remove(states S, args A) Result
	Set(states S, args A) Result
	AddErr(err error, args A) Result
	AddErrState(state string, err error, args A) Result

	EvAdd1(event *Event, state string, args A) Result
	EvAdd(event *Event, states S, args A) Result
	EvRemove1(event *Event, state string, args A) Result
	EvRemove(event *Event, states S, args A) Result
	EvAddErr(event *Event, err error, args A) Result
	EvAddErrState(event *Event, state string, err error, args A) Result

	WhenArgs(state string, args A, ctx context.Context) <-chan struct{}

	Err() error

	IsErr() bool
	Is(states S) bool
	Is1(state string) bool
	Any(states ...S) bool
	Any1(state ...string) bool
	Not(states S) bool
	Not1(state string) bool
	IsTime(time Time, states S) bool
	WasTime(time Time, states S) bool
	IsClock(clock Clock) bool
	WasClock(clock Clock) bool
	Has(states S) bool
	Has1(state string) bool

	When(states S, ctx context.Context) <-chan struct{}
	When1(state string, ctx context.Context) <-chan struct{}
	WhenNot(states S, ctx context.Context) <-chan struct{}
	WhenNot1(state string, ctx context.Context) <-chan struct{}
	WhenTime(
		states S, times Time, ctx context.Context) <-chan struct{}
	WhenTime1(state string, tick uint64, ctx context.Context) <-chan struct{}
	WhenTicks(state string, ticks int, ctx context.Context) <-chan struct{}
	WhenErr(ctx context.Context) <-chan struct{}

	StateNames() S
	StateNamesMatch(re *regexp.Regexp) S
	ActiveStates() S
	Tick(state string) uint64
	Clock(states S) Clock
	Time(states S) Time
	TimeSum(states S) uint64
	NewStateCtx(state string) context.Context
	Export() *Serialized
	Schema() Schema
	Switch(groups ...S) string

	Log(msg string, args ...any)
	Id() string
	ParentId() string
	Tags() []string
	SetLogId(val bool)
	GetLogId() bool
	SetLogger(logger Logger)
	SetLogLevel(lvl LogLevel)
	SetLoggerEmpty(lvl LogLevel)
	SetLoggerSimple(logf func(format string, args ...any), level LogLevel)
	Ctx() context.Context
	String() string
	StringAll() string
	Inspect(states S) string
	Index(states S) []int
	Index1(state string) int
	BindHandlers(handlers any) error
	DetachHandlers(handlers any) error
	HasHandlers() bool
	StatesVerified() bool
	Tracers() []Tracer
	DetachTracer(tracer Tracer) error
	BindTracer(tracer Tracer) error
	AddBreakpoint(added S, removed S)
	Dispose()
	WhenDisposed() <-chan struct{}
	IsDisposed() bool
}

Api is a subset of Machine for alternative implementations. TODO copy docs from Machine.

type Clock added in v0.7.0

type Clock map[string]uint64

Clock is a map of state names to their tick values. It's like Time but indexed by string, instead of int.

type CtxBinding added in v0.8.0

type CtxBinding struct {
	Ctx    context.Context
	Cancel context.CancelFunc
}

type CtxKeyName added in v0.8.0

type CtxKeyName struct{}

type CtxValue added in v0.8.0

type CtxValue struct {
	Id    string
	State string
	Tick  uint64
}

type DefaultRelationsResolver

type DefaultRelationsResolver struct {
	Machine    *Machine
	Transition *Transition
	Index      S
	// contains filtered or unexported fields
}

DefaultRelationsResolver is the default implementation of the RelationsResolver with Add, Remove, Require and After. It can be overridden using Opts.Resolver.

func (*DefaultRelationsResolver) InboundRelationsOf added in v0.12.0

func (rr *DefaultRelationsResolver) InboundRelationsOf(toState string) (
	S, error,
)

InboundRelationsOf returns a list of states pointing to [toState]. Not thread safe.

func (*DefaultRelationsResolver) NewAutoMutation added in v0.12.0

func (rr *DefaultRelationsResolver) NewAutoMutation() (*Mutation, S)

GetAutoMutation implements RelationsResolver.GetAutoMutation.

func (*DefaultRelationsResolver) NewSchema added in v0.12.0

func (rr *DefaultRelationsResolver) NewSchema(schema Schema, states S)

func (*DefaultRelationsResolver) RelationsBetween added in v0.12.0

func (rr *DefaultRelationsResolver) RelationsBetween(
	fromState, toState string,
) ([]Relation, error)

GetRelationsBetween returns a list of outbound relations between fromState -> toState. Not thread safe.

func (*DefaultRelationsResolver) RelationsOf added in v0.12.0

func (rr *DefaultRelationsResolver) RelationsOf(fromState string) (
	[]Relation, error,
)

GetRelationsOf returns a list of relation types of the given state. Not thread safe.

func (*DefaultRelationsResolver) SortStates

func (rr *DefaultRelationsResolver) SortStates(states S)

SortStates implements RelationsResolver.SortStates.

func (*DefaultRelationsResolver) TargetStates added in v0.12.0

func (rr *DefaultRelationsResolver) TargetStates(
	t *Transition, statesToSet, index S,
) S

GetTargetStates implements RelationsResolver.GetTargetStates.

type Event

type Event struct {
	// Name of the event / handler
	Name string
	// MachineId is the ID of the parent machine.
	MachineId string
	// TransitionId is the ID of the parent transition.
	TransitionId string
	// Args is a map of named arguments for a Mutation.
	Args A
	// IsCheck is true if this event is a check event, fired by one of Can*()
	// methods. Useful for avoiding flooding the log with errors.
	IsCheck bool
	// contains filtered or unexported fields
}

Event struct represents a single event of a Mutation within a Transition. One event can have 0-n handlers.

func (*Event) AcceptTimeout added in v0.10.1

func (e *Event) AcceptTimeout() bool

AcceptTimeout is like IsValid, but requires the handler to stop executing after receiving [true].

func (*Event) Clone added in v0.9.0

func (e *Event) Clone() *Event

Clone clones only the essential data of the Event. Useful for tracing vs GC.

func (*Event) IsValid added in v0.10.1

func (e *Event) IsValid() bool

IsValid confirm this event should still be processed. Useful for negotiation handlers, which can't use state context.

func (*Event) Machine

func (e *Event) Machine() *Machine

func (*Event) Mutation added in v0.5.0

func (e *Event) Mutation() *Mutation

Mutation returns the Mutation of an Event.

func (*Event) Transition added in v0.5.0

func (e *Event) Transition() *Transition

Transition returns the Transition of an Event.

type ExceptionArgsPanic

type ExceptionArgsPanic struct {
	CalledStates S
	StatesBefore S
	Transition   *Transition
	LastStep     *Step
	StackTrace   string
}

ExceptionArgsPanic is an optional argument ["panic"] for the Exception state which describes a panic within a Transition handler.

type ExceptionHandler

type ExceptionHandler struct{}

ExceptionHandler provide a basic Exception state support, as should be embedded into handler structs in most of the cases.

func (*ExceptionHandler) ExceptionState

func (eh *ExceptionHandler) ExceptionState(e *Event)

ExceptionState is a final entry handler for the Exception state. Args: - err error: The error that caused the Exception state. - panic *ExceptionArgsPanic: Optional details about the panic.

type HandlerDispose added in v0.9.0

type HandlerDispose func(id string, ctx context.Context)

HandlerDispose is a machine disposal handler func signature.

type HandlerFinal added in v0.8.0

type HandlerFinal func(e *Event)

HandlerFinal is a final transition handler func signature.

type HandlerNegotiation added in v0.8.0

type HandlerNegotiation func(e *Event) bool

HandlerNegotiation is a negotiation transition handler func signature.

type IndexStateCtx added in v0.7.0

type IndexStateCtx map[string]*CtxBinding

IndexStateCtx is a map of (single) state names to a context cancel function

type IndexWhen added in v0.7.0

type IndexWhen map[string][]*WhenBinding

IndexWhen is a map of (single) state names to a list of activation or de-activation bindings

type IndexWhenArgs added in v0.7.0

type IndexWhenArgs map[string][]*WhenArgsBinding

IndexWhenArgs is a map of (single) state names to a list of args value bindings

type IndexWhenTime added in v0.7.0

type IndexWhenTime map[string][]*WhenTimeBinding

IndexWhenTime is a map of (single) state names to a list of time bindings

type LastTxTracer added in v0.12.0

type LastTxTracer struct {
	*NoOpTracer
	// contains filtered or unexported fields
}

TODO add TTL, ctx

func NewLastTxTracer added in v0.12.0

func NewLastTxTracer() *LastTxTracer

NewLastTxTracer returns a Tracer that logs the last transition.

func (*LastTxTracer) Load added in v0.12.0

func (t *LastTxTracer) Load() *Transition

Load returns the last transition.

func (*LastTxTracer) String added in v0.12.0

func (t *LastTxTracer) String() string

func (*LastTxTracer) TransitionEnd added in v0.12.0

func (t *LastTxTracer) TransitionEnd(transition *Transition)

type LogArgsMapper added in v0.8.0

type LogArgsMapper func(args A) map[string]string

LogArgsMapper is a function that maps arguments to be logged. Useful for debugging.

type LogEntry added in v0.6.0

type LogEntry struct {
	Level LogLevel
	Text  string
}

type LogLevel

type LogLevel int

LogLevel enum

const (
	// LogNothing means no logging, including external msgs.
	LogNothing LogLevel = iota
	// LogChanges means logging state changes and external msgs.
	LogChanges
	// LogOps means LogChanges + logging all the operations.
	LogOps
	// LogDecisions means LogOps + logging all the decisions behind them.
	LogDecisions
	// LogEverything means LogDecisions + all event and handler names, and more.
	LogEverything
)

TODO add LogOpsSubs (30), spread log level 0 - 10 - 20 - 30 - 40

func EnvLogLevel added in v0.7.0

func EnvLogLevel(name string) LogLevel

EnvLogLevel returns a log level from an environment variable, AM_LOG by default.

func (LogLevel) String added in v0.3.0

func (l LogLevel) String() string

type Logger

type Logger func(level LogLevel, msg string, args ...any)

Logger is a logging function for the machine.

type Machine

type Machine struct {
	// Maximum number of mutations that can be queued. Default: 1000.
	QueueLimit int
	// HandlerTimeout defined the time for a handler to execute before it causes
	// an Exception. Default: 1s. See also Opts.HandlerTimeout.
	// Using HandlerTimeout can cause race conditions, see Event.IsValid().
	HandlerTimeout time.Duration
	// EvalTimeout is the time the machine will try to execute an eval func.
	// Like any other handler, eval func also has HandlerTimeout. Default: 1s.
	EvalTimeout time.Duration
	// If true, the machine will print all exceptions to stdout. Default: true.
	// Requires an ExceptionHandler binding and Machine.PanicToException set.
	LogStackTrace bool
	// If true, the machine will catch panic and trigger the Exception state.
	// Default: true.
	PanicToException bool
	// DisposeTimeout specifies the duration to wait for the queue to drain during
	// disposal. Default 1s.
	DisposeTimeout time.Duration
	// contains filtered or unexported fields
}

Machine provides a state machine API with mutations, state schema, handlers, subscriptions, tracers, and helpers methods. It also holds a queue of mutations to execute.

func New

func New(ctx context.Context, schema Schema, opts *Opts) *Machine

New creates a new Machine instance, bound to context and modified with optional Opts.

Example
ctx := context.TODO()
mach := New(ctx, Schema{
	"Foo": {Require: S{"Bar"}},
	"Bar": {},
}, nil)
mach.Add1("Foo", nil)
mach.Is1("Foo") // false

func NewCommon added in v0.5.0

func NewCommon(
	ctx context.Context, id string, stateSchema Schema, stateNames S,
	handlers any, parent Api, opts *Opts,
) (*Machine, error)

NewCommon creates a new Machine instance with all the common options set.

Example
// define (tip: use am-gen instead)
stateStruct := Schema{
	"Foo": {Require: S{"Bar"}},
	"Bar": {},
}
stateNames := []string{"Foo", "Bar"}
type Handlers struct{}
// func (h *Handlers) FooState(e *Event) {
// 	args := e.Args
// 	mach := e.Machine()
// 	ctx := mach.NewStateCtx("Foo")
// 	// unblock
// 	go func() {
// 		if ctx.Err() != nil {
// 			return // expired
// 		}
// 		// blocking calls...
// 		if ctx.Err() != nil {
// 			return // expired
// 		}
// 		mach.Add1("Bar", nil)
// 	}()
// }

// init
ctx := context.TODO()
mach, err := NewCommon(ctx, "mach-id",
	stateStruct, stateNames,
	&Handlers{}, nil, nil)
if err != nil {
	panic(err)
}
mach.SetLogLevel(LogOps)

// debug
// import amhelp "github.com/pancsta/asyncmachine-go/pkg/helpers"
// amhelp.EnableDebugging(false)
// amhelp.MachDebugEnv(mach)

func (*Machine) ActiveStates

func (m *Machine) ActiveStates() S

ActiveStates returns a copy of the currently active states.

func (*Machine) Add

func (m *Machine) Add(states S, args A) Result

Add activates a list of states in the machine, returning the result of the transition (Executed, Queued, Canceled). Like every mutation method, it will resolve relations and trigger handlers.

func (*Machine) Add1 added in v0.5.0

func (m *Machine) Add1(state string, args A) Result

Add1 is a shorthand method to add a single state with the passed args.

func (*Machine) AddBreakpoint added in v0.8.0

func (m *Machine) AddBreakpoint(added S, removed S)

AddBreakpoint adds a breakpoint for an outcome of mutation (added and removed states). Once such mutation happens, a log message will be printed out. You can set an IDE's breakpoint on this line and see the mutation's sync stack trace. When Machine.LogStackTrace is set, the stack trace will be printed out as well. Many breakpoints can be added, but none removed.

func (*Machine) AddErr

func (m *Machine) AddErr(err error, args A) Result

AddErr is a dedicated method to add the Exception state with the passed error and optional arguments. Like every mutation method, it will resolve relations and trigger handlers. AddErr produces a stack trace of the error, if LogStackTrace is enabled.

func (*Machine) AddErrState added in v0.7.0

func (m *Machine) AddErrState(state string, err error, args A) Result

AddErrState adds a dedicated error state, along with the build in Exception state. Like every mutation method, it will resolve relations and trigger handlers. AddErrState produces a stack trace of the error, if LogStackTrace is enabled.

func (*Machine) Any

func (m *Machine) Any(states ...S) bool

Any a is group call to Is, returns true if any of the params return true from Is.

machine.StringAll() // ()[Foo:0 Bar:0 Baz:0]
machine.Add(S{"Foo"})
// is(Foo, Bar) or is(Bar)
machine.Any(S{"Foo", "Bar"}, S{"Bar"}) // false
// is(Foo) or is(Bar)
machine.Any(S{"Foo"}, S{"Bar"}) // true

func (*Machine) Any1 added in v0.5.0

func (m *Machine) Any1(states ...string) bool

Any1 is group call to Is1(), returns true if any of the params return true from Is1().

func (*Machine) BindHandlers

func (m *Machine) BindHandlers(handlers any) error

BindHandlers binds a struct of handler methods to machine's states, based on the naming convention, eg `FooState(e *Event)`. Negotiation handlers can optionally return bool.

func (*Machine) BindTracer added in v0.8.0

func (m *Machine) BindTracer(tracer Tracer) error

BindTracer binds a Tracer to the machine. Tracers can cause Exception in submachines, before any handlers are bound. Use the Err() getter to examine such errors.

func (*Machine) Clock

func (m *Machine) Clock(states S) Clock

Clock returns current machine's clock, a state-keyed map of ticks. If states are passed, only the ticks of the passed states are returned.

func (*Machine) CountActive added in v0.8.0

func (m *Machine) CountActive(states S) int

CountActive returns the amount of active states from a passed list. Useful for state groups.

func (*Machine) Ctx

func (m *Machine) Ctx() context.Context

Ctx return machine's root context.

func (*Machine) DetachHandlers added in v0.8.0

func (m *Machine) DetachHandlers(handlers any) error

DetachHandlers detaches previously bound machine handlers.

func (*Machine) DetachTracer added in v0.8.0

func (m *Machine) DetachTracer(tracer Tracer) error

DetachTracer tries to remove a tracer from the machine. Returns an error in case the tracer wasn't bound.

func (*Machine) Dispose

func (m *Machine) Dispose()

Dispose disposes the machine and all its emitters. You can wait for the completion of the disposal with `<-mach.WhenDisposed`.

func (*Machine) DisposeForce added in v0.5.0

func (m *Machine) DisposeForce()

DisposeForce disposes the machine and all its emitters, without waiting for the queue to drain. Will cause panics.

func (*Machine) Err

func (m *Machine) Err() error

Err returns the last error.

func (*Machine) EvAdd added in v0.9.0

func (m *Machine) EvAdd(event *Event, states S, args A) Result

EvAdd is like Add, but passed the source event as the 1st param, which results in traceable transitions.

func (*Machine) EvAdd1 added in v0.9.0

func (m *Machine) EvAdd1(event *Event, state string, args A) Result

EvAdd1 is like Add1 but passed the source event as the 1st param, which results in traceable transitions.

func (*Machine) EvAddErr added in v0.9.0

func (m *Machine) EvAddErr(event *Event, err error, args A) Result

EvAddErr is like AddErr, but passed the source event as the 1st param, which results in traceable transitions.

func (*Machine) EvAddErrState added in v0.9.0

func (m *Machine) EvAddErrState(
	event *Event, state string, err error, args A,
) Result

EvAddErrState is like AddErrState, but passed the source event as the 1st param, which results in traceable transitions.

func (*Machine) EvRemove added in v0.9.0

func (m *Machine) EvRemove(event *Event, states S, args A) Result

EvRemove is like Remove but passed the source event as the 1st param, which results in traceable transitions.

func (*Machine) EvRemove1 added in v0.9.0

func (m *Machine) EvRemove1(event *Event, state string, args A) Result

EvRemove1 is like Remove1, but passed the source event as the 1st param, which results in traceable transitions.

func (*Machine) Eval added in v0.5.0

func (m *Machine) Eval(source string, fn func(), ctx context.Context) bool

Eval executes a function on the machine's queue, allowing to avoid using locks for non-handler code. Blocking code should NOT be scheduled here. Eval cannot be called within a handler's critical section, as both are using the same serial queue and will deadlock. Eval has a timeout of HandlerTimeout/2 and will return false in case it happens.

ctx: nil context defaults to machine's context.

Note: usage of Eval is discouraged. But if you have to, use AM_DETECT_EVAL in tests for deadlock detection. Most usages of eval can be replaced with atomics or returning from mutation via channels.

func (*Machine) Export added in v0.6.4

func (m *Machine) Export() *Serialized

Export exports the machine state as Serialized: ID, machine time, and state names.

func (*Machine) GetLogArgs added in v0.5.0

func (m *Machine) GetLogArgs() LogArgsMapper

GetLogArgs returns the current log args function.

func (*Machine) GetLogId added in v0.8.0

func (m *Machine) GetLogId() bool

GetLogId returns the current state of the log ID setting.

func (*Machine) HandleDispose added in v0.9.0

func (m *Machine) HandleDispose(fn HandlerDispose)

HandleDispose adds a function to be called after the machine gets disposed. These functions will run synchronously just before WhenDisposed() channel gets closed. Considering it's a low-level feature, its advaised to handle disposal via dedicated states.

func (*Machine) Has

func (m *Machine) Has(states S) bool

Has return true is passed states are registered in the machine. Useful for checking if a machine implements a specific state set.

func (*Machine) Has1 added in v0.5.0

func (m *Machine) Has1(state string) bool

Has1 is a shorthand for Has. It returns true if the passed state is registered in the machine.

func (*Machine) HasHandlers added in v0.12.0

func (m *Machine) HasHandlers() bool

HasHandlers returns true if this machine has bound handlers, and thus an allocated goroutine. It also makes it nondeterministic.

func (*Machine) Id added in v0.8.0

func (m *Machine) Id() string

Id returns the machine's id.

func (*Machine) Import added in v0.6.4

func (m *Machine) Import(data *Serialized) error

Import imports the machine state from Serialized. It's not safe to import into a machine which has already produces transitions and/or has telemetry connected.

func (*Machine) Index added in v0.7.0

func (m *Machine) Index(states S) []int

Index returns a list of state indexes in the machine's StateNames() list, with -1s for missing ones.

func (*Machine) Index1 added in v0.12.0

func (m *Machine) Index1(state string) int

Index1 returns the index of a state in the machine's StateNames() list, or -1 when not found or machine has been disposed.

func (*Machine) Inspect

func (m *Machine) Inspect(states S) string

Inspect returns a multi-line string representation of the machine (states, relations, clocks). states: param for ordered or partial results.

func (*Machine) InternalLog added in v0.12.0

func (m *Machine) InternalLog(lvl LogLevel, msg string, args ...any)

InternalLog adds an internal log entry from the outside. It should be used only by packages extending pkg/machine. Use Log instead.

func (*Machine) Is

func (m *Machine) Is(states S) bool

Is checks if all the passed states are currently active.

machine.StringAll() // ()[Foo:0 Bar:0 Baz:0]
machine.Add(S{"Foo"})
machine.Is(S{"Foo"}) // true
machine.Is(S{"Foo", "Bar"}) // false

func (*Machine) Is1 added in v0.5.0

func (m *Machine) Is1(state string) bool

Is1 is a shorthand method to check if a single state is currently active. See Is().

func (*Machine) IsClock

func (m *Machine) IsClock(clock Clock) bool

IsClock checks if the machine has changed since the passed clock. Returns true if at least one state has changed.

func (*Machine) IsDisposed added in v0.8.0

func (m *Machine) IsDisposed() bool

IsDisposed returns true if the machine has been disposed.

func (*Machine) IsErr

func (m *Machine) IsErr() bool

IsErr checks if the machine has the Exception state currently active.

func (*Machine) IsQueued

func (m *Machine) IsQueued(mutationType MutationType, states S,
	withoutArgsOnly bool, statesStrictEqual bool, startIndex int,
) int

IsQueued checks if a particular mutation has been queued. Returns an index of the match or -1 if not found.

mutationType: add, remove, set

states: list of states used in the mutation

withoutParamsOnly: matches only mutation without the arguments object

statesStrictEqual: states of the mutation have to be exactly like `states` and not a superset.

func (*Machine) IsTime added in v0.7.0

func (m *Machine) IsTime(t Time, states S) bool

IsTime checks if the machine has changed since the passed time (list of ticks). Returns true if at least one state has changed. The states param is optional and can be used to check only a subset of states.

func (*Machine) Log

func (m *Machine) Log(msg string, args ...any)

Log logs an [extern] message unless LogNothing is set (default). Optionally redirects to a custom logger from SetLogger.

func (*Machine) LogLevel

func (m *Machine) LogLevel() LogLevel

LogLevel returns the log level of the machine.

func (*Machine) Logger added in v0.12.0

func (m *Machine) Logger() Logger

Logger returns the current custom logger function, or nil.

func (*Machine) MustBindHandlers added in v0.8.0

func (m *Machine) MustBindHandlers(handlers any)

MustBindHandlers is a panicking version of BindHandlers, useful in tests.

func (*Machine) MustParseStates

func (m *Machine) MustParseStates(states S) S

MustParseStates parses the states and returns them as a list. Panics when a state is not defined. It's an usafe equivalent of VerifyStates.

func (*Machine) NewStateCtx added in v0.5.0

func (m *Machine) NewStateCtx(state string) context.Context

NewStateCtx returns a new sub-context, bound to the current clock's tick of the passed state.

Context cancels when the state has been deactivated, or right away, if it isn't currently active.

State contexts are used to check state expirations and should be checked often inside goroutines.

func (*Machine) Not

func (m *Machine) Not(states S) bool

Not checks if **none** of the passed states are currently active.

machine.StringAll()
// -> ()[A:0 B:0 C:0 D:0]
machine.Add(S{"A", "B"})

// not(A) and not(C)
machine.Not(S{"A", "C"})
// -> false

// not(C) and not(D)
machine.Not(S{"C", "D"})
// -> true

func (*Machine) Not1 added in v0.5.0

func (m *Machine) Not1(state string) bool

Not1 is a shorthand method to check if a single state is currently inactive. See Not().

func (*Machine) PanicToErr added in v0.7.0

func (m *Machine) PanicToErr(args A)

PanicToErr will catch a panic and add the Exception state. Needs to be called in a defer statement, just like a recover() call.

func (*Machine) PanicToErrState added in v0.7.0

func (m *Machine) PanicToErrState(state string, args A)

PanicToErrState will catch a panic and add the Exception state, along with the passed state. Needs to be called in a defer statement, just like a recover() call.

func (*Machine) ParentId added in v0.8.0

func (m *Machine) ParentId() string

ParentId returns the ID of the parent machine (if any).

func (*Machine) Queue

func (m *Machine) Queue() []*Mutation

Queue returns a copy of the currently active states.

func (*Machine) Remove

func (m *Machine) Remove(states S, args A) Result

Remove deactivates a list of states in the machine, returning the result of the transition (Executed, Queued, Canceled). Like every mutation method, it will resolve relations and trigger handlers.

func (*Machine) Remove1 added in v0.5.0

func (m *Machine) Remove1(state string, args A) Result

Remove1 is a shorthand method to remove a single state with the passed args. See Remove().

func (*Machine) Resolver

func (m *Machine) Resolver() RelationsResolver

Resolver returns the relation resolver, used to produce target states of transitions.

func (*Machine) Schema added in v0.11.0

func (m *Machine) Schema() Schema

Schema returns a copy of machine's schema.

func (*Machine) SchemaVer added in v0.10.1

func (m *Machine) SchemaVer() int

SchemaVer return the current version of the schema.

func (*Machine) Set

func (m *Machine) Set(states S, args A) Result

Set deactivates a list of states in the machine, returning the result of the transition (Executed, Queued, Canceled). Like every mutation method, it will resolve relations and trigger handlers.

func (*Machine) SetLogArgs added in v0.5.0

func (m *Machine) SetLogArgs(mapper LogArgsMapper)

SetLogArgs accepts a function which decides which mutation arguments to log. See NewArgsMapper or create your own manually.

func (*Machine) SetLogId added in v0.8.0

func (m *Machine) SetLogId(val bool)

SetLogId enables or disables the logging of the machine's ID in log messages.

func (*Machine) SetLogLevel

func (m *Machine) SetLogLevel(level LogLevel)

SetLogLevel sets the log level of the machine.

func (*Machine) SetLogger

func (m *Machine) SetLogger(fn Logger)

SetLogger sets a custom logger function.

func (*Machine) SetLoggerEmpty added in v0.7.0

func (m *Machine) SetLoggerEmpty(level LogLevel)

SetLoggerEmpty creates an empty logger that does nothing and sets the log level in one call. Useful when combined with am-dbg. Requires LogChanges log level to produce any output.

func (*Machine) SetLoggerSimple added in v0.7.0

func (m *Machine) SetLoggerSimple(
	logf func(format string, args ...any), level LogLevel,
)

SetLoggerSimple takes log.Printf and sets the log level in one call. Useful for testing. Requires LogChanges log level to produce any output.

func (*Machine) SetSchema added in v0.11.0

func (m *Machine) SetSchema(newSchema Schema, names S) error

SetSchema sets the machine's schema. It will automatically call VerifyStates with the names param and handle EventSchemaChange if successful. The new schema has to be longer than the previous one (no relations-only changes). The length of the schema is also the version of the schema.

func (*Machine) SetTags added in v0.10.1

func (m *Machine) SetTags(tags []string)

SetTags updates the machine's tags with the provided slice of strings.

func (*Machine) StateNames

func (m *Machine) StateNames() S

StateNames returns a copy of all the state names.

func (*Machine) StateNamesMatch added in v0.12.0

func (m *Machine) StateNamesMatch(re *regexp.Regexp) S

TODO docs

func (*Machine) StatesVerified added in v0.5.0

func (m *Machine) StatesVerified() bool

StatesVerified returns true if the state names have been ordered using VerifyStates.

func (*Machine) String

func (m *Machine) String() string

String returns a one line representation of the currently active states, with their clock values. Inactive states are omitted. Eg: (Foo:1 Bar:3)

func (*Machine) StringAll

func (m *Machine) StringAll() string

StringAll returns a one line representation of all the states, with their clock values. Inactive states are in square brackets.

(Foo:1 Bar:3) [Baz:2]

func (*Machine) Switch added in v0.3.0

func (m *Machine) Switch(groups ...S) string

Switch returns the first state from the passed list that is currently active, making it handy for switch statements. Useful for state groups.

switch mach.Switch(ss.GroupPlaying) {
case "Playing":
case "Paused":
case "Stopped":
}

func (*Machine) Tags added in v0.5.0

func (m *Machine) Tags() []string

Tags returns machine's tags, a list of unstructured strings without spaces.

func (*Machine) Tick added in v0.7.0

func (m *Machine) Tick(state string) uint64

Tick return the current tick for a given state.

func (*Machine) Time

func (m *Machine) Time(states S) Time

Time returns machine's time, a list of ticks per state. Returned value includes the specified states, or all the states if nil.

func (*Machine) TimeSum

func (m *Machine) TimeSum(states S) uint64

TimeSum returns the sum of machine's time (ticks per state). Returned value includes the specified states, or all the states if nil. It's a very inaccurate, yet simple way to measure the machine's time.

func (*Machine) Toggle added in v0.10.1

func (m *Machine) Toggle(states S, args A) Result

Toggle deactivates a list of states in case all are active, or activates all otherwise. Returns the result of the transition (Executed, Queued, Canceled).

func (*Machine) Toggle1 added in v0.8.0

func (m *Machine) Toggle1(state string, args A) Result

Toggle1 activates or deactivates a single state, depending on its current state. Returns the result of the transition (Executed, Queued, Canceled).

func (*Machine) Tracers added in v0.5.0

func (m *Machine) Tracers() []Tracer

Tracers return a copy of currenty attached tracers.

func (*Machine) Transition

func (m *Machine) Transition() *Transition

Transition returns the current transition, if any.

func (*Machine) VerifyStates added in v0.3.0

func (m *Machine) VerifyStates(states S) error

VerifyStates verifies an array of state names and returns an error in case at least one isn't defined. It also retains the order and uses it for StateNames. Verification can be checked via Machine.StatesVerified.

func (*Machine) WasClock added in v0.12.0

func (m *Machine) WasClock(clock Clock) bool

WasClock checks if the passed time has happened (or happening right now). Returns false if at least one state is too early.

func (*Machine) WasTime added in v0.12.0

func (m *Machine) WasTime(t Time, states S) bool

WasTime checks if the passed time has happened (or happening right now). Returns false if at least one state is too early. The states param is optional and can be used to check only a subset of states.

func (*Machine) When

func (m *Machine) When(states S, ctx context.Context) <-chan struct{}

When returns a channel that will be closed when all the passed states become active or the machine gets disposed.

ctx: optional context that will close the channel when handlerLoopDone.

func (*Machine) When1 added in v0.5.0

func (m *Machine) When1(state string, ctx context.Context) <-chan struct{}

When1 is an alias to When() for a single state. See When.

func (*Machine) WhenArgs added in v0.5.0

func (m *Machine) WhenArgs(
	state string, args A, ctx context.Context,
) <-chan struct{}

WhenArgs returns a channel that will be closed when the passed state becomes active with all the passed args. Args are compared using the native '=='. It's meant to be used with async Multi states, to filter out a specific call.

ctx: optional context that will close the channel when handler loop ends.

func (*Machine) WhenDisposed added in v0.5.0

func (m *Machine) WhenDisposed() <-chan struct{}

WhenDisposed returns a channel that will be closed when the machine is disposed. Requires bound handlers. Use Machine.disposed in case no handlers have been bound.

func (*Machine) WhenErr

func (m *Machine) WhenErr(disposeCtx context.Context) <-chan struct{}

WhenErr returns a channel that will be closed when the machine is in the Exception state.

ctx: optional context that will close the channel when handlerLoopDone.

func (*Machine) WhenNot

func (m *Machine) WhenNot(states S, ctx context.Context) <-chan struct{}

WhenNot returns a channel that will be closed when all the passed states become inactive or the machine gets disposed.

ctx: optional context that will close the channel when handlerLoopDone.

func (*Machine) WhenNot1 added in v0.5.0

func (m *Machine) WhenNot1(state string, ctx context.Context) <-chan struct{}

WhenNot1 is an alias to WhenNot() for a single state. See WhenNot.

func (*Machine) WhenQueueEnds added in v0.5.0

func (m *Machine) WhenQueueEnds(ctx context.Context) <-chan struct{}

WhenQueueEnds closes every time the queue ends, or the optional ctx expires.

ctx: optional context that will close the channel when handlerLoopDone.

func (*Machine) WhenTicks added in v0.5.0

func (m *Machine) WhenTicks(
	state string, ticks int, ctx context.Context,
) <-chan struct{}

WhenTicks waits N ticks of a single state (relative to now). Uses WhenTime underneath.

ctx: optional context that will close the channel when handlerLoopDone.

func (*Machine) WhenTime added in v0.5.0

func (m *Machine) WhenTime(
	states S, times Time, ctx context.Context,
) <-chan struct{}

WhenTime returns a channel that will be closed when all the passed states have passed the specified time. The time is a logical clock of the state. Machine time can be sourced from Machine.Time(), or Machine.Clock().

ctx: optional context that will close the channel when handlerLoopDone.

func (*Machine) WhenTime1 added in v0.11.0

func (m *Machine) WhenTime1(
	state string, ticks uint64, ctx context.Context,
) <-chan struct{}

WhenTime1 waits till ticks for a single state equal the given value (or more).

ctx: optional context that will close the channel when handlerLoopDone.

func (*Machine) WillBe added in v0.8.0

func (m *Machine) WillBe(states S) bool

WillBe returns true if the passed states are scheduled to be activated. See IsQueued to perform more detailed queries.

func (*Machine) WillBe1 added in v0.8.0

func (m *Machine) WillBe1(state string) bool

WillBe1 returns true if the passed state is scheduled to be activated. See IsQueued to perform more detailed queries.

func (*Machine) WillBeRemoved added in v0.8.0

func (m *Machine) WillBeRemoved(states S) bool

WillBeRemoved returns true if the passed states are scheduled to be deactivated. See IsQueued to perform more detailed queries.

func (*Machine) WillBeRemoved1 added in v0.8.0

func (m *Machine) WillBeRemoved1(state string) bool

WillBeRemoved1 returns true if the passed state is scheduled to be deactivated. See IsQueued to perform more detailed queries.

type MutSource added in v0.9.0

type MutSource struct {
	MachId string
	TxId   string
	// Machine time of the source machine BEFORE the event.
	MachTime uint64
}

type Mutation

type Mutation struct {
	// add, set, remove
	Type MutationType
	// States explicitly passed to the mutation method, as their indexes. Use
	// Transition.CalledStates or IndexToStates to get the actual state names.
	Called []int
	// argument map passed to the mutation method (if any).
	Args A
	// this mutation has been triggered by an auto state
	Auto bool
	// Source is the source event for this mutation.
	Source *MutSource
	// contains filtered or unexported fields
}

Mutation represents an atomic change (or an attempt) of machine's active states. Mutation causes a Transition.

func (Mutation) CalledIndex added in v0.12.0

func (m Mutation) CalledIndex(index S) *TimeIndex

func (Mutation) IsCalled added in v0.12.0

func (m Mutation) IsCalled(idx int) bool

func (Mutation) String added in v0.11.0

func (m Mutation) String() string

func (Mutation) StringFromIndex added in v0.12.0

func (m Mutation) StringFromIndex(index S) string

type MutationType

type MutationType int

MutationType enum

const (
	MutationAdd MutationType = iota
	MutationRemove
	MutationSet
)

func (MutationType) String

func (m MutationType) String() string

type NoOpTracer added in v0.6.0

type NoOpTracer struct{}

NoOpTracer is a no-op implementation of Tracer, used for embedding.

func (*NoOpTracer) HandlerEnd added in v0.6.0

func (t *NoOpTracer) HandlerEnd(
	transition *Transition, emitter string, handler string)

func (*NoOpTracer) HandlerStart added in v0.6.0

func (t *NoOpTracer) HandlerStart(
	transition *Transition, emitter string, handler string)

func (*NoOpTracer) Inheritable added in v0.6.0

func (t *NoOpTracer) Inheritable() bool

func (*NoOpTracer) MachineDispose added in v0.6.0

func (t *NoOpTracer) MachineDispose(machID string)

func (*NoOpTracer) MachineInit added in v0.6.0

func (t *NoOpTracer) MachineInit(machine Api) context.Context

func (*NoOpTracer) NewSubmachine added in v0.6.0

func (t *NoOpTracer) NewSubmachine(parent, machine Api)

func (*NoOpTracer) QueueEnd added in v0.6.0

func (t *NoOpTracer) QueueEnd(machine Api)

func (*NoOpTracer) SchemaChange added in v0.11.0

func (t *NoOpTracer) SchemaChange(machine Api, old Schema)

func (*NoOpTracer) TransitionEnd added in v0.6.0

func (t *NoOpTracer) TransitionEnd(transition *Transition)

func (*NoOpTracer) TransitionInit added in v0.6.0

func (t *NoOpTracer) TransitionInit(transition *Transition)

func (*NoOpTracer) TransitionStart added in v0.7.0

func (t *NoOpTracer) TransitionStart(transition *Transition)

func (*NoOpTracer) VerifyStates added in v0.7.0

func (t *NoOpTracer) VerifyStates(machine Api)

type Opts

type Opts struct {
	// Unique ID of this machine. Default: random ID.
	Id string
	// Time for a handler to execute. Default: time.Second
	HandlerTimeout time.Duration
	// If true, the machine will NOT print all exceptions to stdout.
	DontLogStackTrace bool
	// If true, the machine will die on panics.
	DontPanicToException bool
	// If true, the machine will NOT prefix its logs with its ID.
	// TODO refac to DontLogId
	DontLogID bool
	// Custom relations resolver. Default: *DefaultRelationsResolver.
	Resolver RelationsResolver
	// Log level of the machine. Default: LogNothing.
	LogLevel LogLevel
	// Tracer for the machine. Default: nil.
	Tracers []Tracer
	// LogArgs matching function for the machine. Default: nil.
	LogArgs func(args A) map[string]string
	// Parent machine, used to inherit certain properties, e.g. tracers.
	// Overrides ParentID. Default: nil.
	Parent Api
	// ParentID is the ID of the parent machine. Default: "".
	ParentId string
	// Tags is a list of tags for the machine. Default: nil.
	Tags []string
	// QueueLimit is the maximum number of mutations that can be queued.
	// Default: 1000.
	// TODO per-state QueueLimit
	QueueLimit int
	// DetectEval will detect Eval calls directly in handlers, which causes a
	// deadlock. It works in similar way as -race flag in Go and can also be
	// triggered by setting either env var: AM_DEBUG=1 or AM_DETECT_EVAL=1.
	// Default: false.
	DetectEval bool
}

Opts struct is used to configure a new Machine.

func OptsWithDebug added in v0.5.0

func OptsWithDebug(opts *Opts) *Opts

OptsWithDebug returns Opts with debug settings (DontPanicToException, long HandlerTimeout).

func OptsWithTracers added in v0.5.0

func OptsWithTracers(opts *Opts, tracers ...Tracer) *Opts

OptsWithTracers returns Opts with the given tracers. Tracers are inherited by submachines (via Opts.Parent) when env.AM_DEBUG is set.

type Relation

type Relation int8

Relation enum

const (
	// TODO refac, 0 should be RelationNone, start from 1, udpate with dbg proto
	RelationAfter Relation = iota
	RelationAdd
	RelationRequire
	RelationRemove
)

func (Relation) String

func (r Relation) String() string

type RelationsResolver

type RelationsResolver interface {
	// TargetStates returns the target states after parsing the relations.
	TargetStates(t *Transition, calledStates, index S) S
	// NewAutoMutation returns an (optional) auto mutation which is appended to
	// the queue after the transition is executed. It also returns the names of
	// the called states.
	NewAutoMutation() (*Mutation, S)
	// SortStates sorts the states in the order their handlers should be
	// executed.
	SortStates(states S)
	// RelationsOf returns a list of relation types of the given state.
	RelationsOf(fromState string) ([]Relation, error)
	// RelationsBetween returns a list of relation types between the given
	// states.
	RelationsBetween(fromState, toState string) ([]Relation, error)
	InboundRelationsOf(toState string) (S, error)

	// NewSchema runs when Machine receives a new struct.
	NewSchema(schema Schema, states S)
}

RelationsResolver is an interface for parsing relations between states. Not thread-safe. TODO support custom relation types and additional state properties.

type Result

type Result int8

Result enum is the result of a state Transition.

const (
	// Executed means that the transition was executed immediately and not
	// canceled.
	Executed Result = 1 << iota
	// Canceled means that the transition was canceled, by either relations or a
	// handler.
	Canceled
	// Queued means that the transition was queued for later execution. The
	// following methods can be used to wait for the results:
	// - Machine.When
	// - Machine.WhenNot
	// - Machine.WhenArgs
	// - Machine.WhenTime
	// - Machine.WhenTicks
	// - Machine.WhenTicksEq
	Queued
	// ResultNoOp means that the transition was a no-op, i.e. the state was
	// already active. ResultNoOp is only used by helpers, and never returned by
	// the machine itself.
	ResultNoOp
)

func (Result) String

func (r Result) String() string

type S

type S []string

S (state names) is a string list of state names.

func DiffStates

func DiffStates(states1 S, states2 S) S

DiffStates returns the states that are in states1 but not in states2.

func IndexToStates added in v0.12.0

func IndexToStates(index S, states []int) S

IndexToStates decodes state indexes based on the provided index.

func SAdd added in v0.7.0

func SAdd(states ...S) S

SAdd concatenates multiple state lists into one, removing duplicates. Useful for merging lists of states, eg a state group with other states involved in a relation.

func SRem added in v0.12.0

func SRem(src S, states ...S) S

SRem removes groups > 1 from nr 1.

type Schema added in v0.10.2

type Schema = map[string]State

Schema is a map of state names to state definitions.

func CloneSchema added in v0.12.0

func CloneSchema(stateStruct Schema) Schema

CloneSchema deep clones the states struct and returns a copy.

func ParseSchema added in v0.12.0

func ParseSchema(schema Schema) Schema

func SchemaMerge added in v0.10.3

func SchemaMerge(schemas ...Schema) Schema

SchemaMerge merges multiple state structs into one, overriding the previous state definitions. No relation-level merging takes place.

type Serialized added in v0.7.0

type Serialized struct {
	// ID is the ID of a state machine.
	ID string `json:"id" yaml:"id" toml:"id"`
	// Time represents machine time - a list of state activation counters.
	Time Time `json:"time" yaml:"time" toml:"time"`
	// StateNames is an ordered list of state names.
	StateNames S `json:"state_names" yaml:"state_names" toml:"state_names"`
}

Serialized is a machine state serialized to a JSON/YAML/TOML compatible struct. One also needs the state Struct to re-create a state machine.

type State

type State struct {
	Auto    bool     `json:"auto,omitempty"`
	Multi   bool     `json:"multi,omitempty"`
	Require S        `json:"require,omitempty"`
	Add     S        `json:"add,omitempty"`
	Remove  S        `json:"remove,omitempty"`
	After   S        `json:"after,omitempty"`
	Tags    []string `json:"tags,omitempty"`
}

State defines a single state of a machine, its properties and relations.

func StateAdd added in v0.7.0

func StateAdd(source State, overlay State) State

StateAdd adds new states to relations of the source state, without removing existing ones. Useful for adjusting shared stated to a specific machine. Only "true" values for Auto and Multi are applied from [overlay].

func StateSet added in v0.7.0

func StateSet(source State, auto, multi bool, overlay State) State

StateSet replaces passed relations and properties of the source state. Only relations in the overlay state are replaced, the rest is preserved. If [overlay] has all fields `nil`, then only [auto] and [multi] get applied.

type StateIsActive added in v0.7.0

type StateIsActive map[string]bool

TODO optimize indexes

type States

type States interface {
	// Names returns the state names of the state machine.
	Names() S
	SetNames(S)
}

type StatesBase added in v0.8.0

type StatesBase struct {
	// Exception is the only built-in state and mean a global error. All errors
	// have to [State.Require] the Exception state. If [Machine.PanicToErr] is
	// true, Exception will receive it.
	Exception string
	// contains filtered or unexported fields
}

func (*StatesBase) Names added in v0.8.0

func (b *StatesBase) Names() S

func (*StatesBase) SetNames added in v0.8.0

func (b *StatesBase) SetNames(names S)

type Step added in v0.5.0

type Step struct {
	Type StepType
	// Only for Type == StepRelation.
	RelType Relation
	// marks a final handler (FooState, FooEnd)
	IsFinal bool
	// marks a self handler (FooFoo)
	IsSelf bool
	// marks an enter handler (FooState, but not FooEnd). Requires IsFinal.
	IsEnter bool
	// Deprecated, use GetFromState(). TODO remove
	FromState string
	// TODO implement
	FromStateIdx int
	// Deprecated, use GetToState(). TODO remove
	ToState string
	// TODO implement
	ToStateIdx int
	// Deprecated, use RelType. TODO remove
	Data any
}

Step struct represents a single step within a Transition, either a relation resolving step or a handler call.

func (*Step) GetFromState added in v0.9.0

func (s *Step) GetFromState(index S) string

GetFromState returns the source state of a step. Optional, unless no GetToState().

func (*Step) GetToState added in v0.9.0

func (s *Step) GetToState(index S) string

GetToState returns the target state of a step. Optional, unless no GetFromState().

func (*Step) StringFromIndex added in v0.12.0

func (s *Step) StringFromIndex(idx S) string

type StepType added in v0.5.0

type StepType int8

StepType enum

const (
	StepRelation StepType = 1 << iota
	StepHandler
	// TODO rename to StepActivate
	StepSet
	// StepRemove indicates a step where a state goes active->inactive
	// TODO rename to StepDeactivate
	StepRemove
	// StepRemoveNotActive indicates a step where a state goes inactive->inactive
	// TODO rename to StepDeactivatePassive
	StepRemoveNotActive
	StepRequested
	StepCancel
)

func (StepType) String added in v0.5.0

func (tt StepType) String() string

type Time added in v0.7.0

type Time []uint64

Time is machine time, an ordered list of state ticks. It's like Clock, but indexed by int, instead of string. TODO use math/big?

func IndexToTime added in v0.12.0

func IndexToTime(index S, active []int) Time

IndexToTime returns "virtual time" with selected states active. It's useful to use time methods on a list of states, eg the called ones.

func (Time) ActiveIndex added in v0.12.0

func (t Time) ActiveIndex() []int

ActiveIndex returns a list of active state indexes in this machine time slice.

func (Time) ActiveStates added in v0.12.0

func (t Time) ActiveStates(index S) S

ActiveStates returns a list of active state names in this machine time slice.

func (Time) Add added in v0.10.3

func (t Time) Add(t2 Time) Time

Add sums 2 instances of Time and returns a new one.

func (Time) Any1 added in v0.8.0

func (t Time) Any1(idxs ...int) bool

Any1 see Machine.Any1.

func (Time) DiffSince added in v0.10.3

func (t Time) DiffSince(before Time) Time

DiffSince returns the number of ticks for each state in Time since the passed machine time.

func (Time) Get added in v0.12.0

func (t Time) Get(idx int) uint64

Get returns the tick at the given index, or 0 if out of bounds (for old schemas).

func (Time) Is added in v0.7.0

func (t Time) Is(idxs []int) bool

Is checks if all the passed states were active at a given time, via indexes. See Machine.Index().

func (Time) Is1 added in v0.7.0

func (t Time) Is1(idx int) bool

Is1 checks if a state is active at a given time, via its index. See Machine.Index().

func (Time) Not added in v0.12.0

func (t Time) Not(idxs []int) bool

TODO docs

func (Time) Not1 added in v0.12.0

func (t Time) Not1(idx int) bool

TODO docs

func (Time) String added in v0.8.0

func (t Time) String() string

func (Time) Sum added in v0.9.0

func (t Time) Sum() uint64

Sum returns the sum of all the ticks in Time.

func (Time) TimeSum added in v0.12.0

func (t Time) TimeSum(idxs []int) uint64

type TimeIndex added in v0.12.0

type TimeIndex struct {
	Time
	Index S
}

TimeIndex is Time with a bound state index (list of state names). It's not suitable for storage, use Time instead.

func NewTimeIndex added in v0.12.0

func NewTimeIndex(index S, activeStates []int) *TimeIndex

func (TimeIndex) ActiveStates added in v0.12.0

func (t TimeIndex) ActiveStates() S

func (TimeIndex) Any1 added in v0.12.0

func (t TimeIndex) Any1(states ...string) bool

func (TimeIndex) Is added in v0.12.0

func (t TimeIndex) Is(states S) bool

func (TimeIndex) Is1 added in v0.12.0

func (t TimeIndex) Is1(state string) bool

func (TimeIndex) Not added in v0.12.0

func (t TimeIndex) Not(states S) bool

func (TimeIndex) Not1 added in v0.12.0

func (t TimeIndex) Not1(state string) bool

func (TimeIndex) StateName added in v0.12.0

func (t TimeIndex) StateName(idx int) string

func (TimeIndex) String added in v0.12.0

func (t TimeIndex) String() string

func (TimeIndex) TimeSum added in v0.12.0

func (t TimeIndex) TimeSum(states S) uint64

type Tracer added in v0.5.0

type Tracer interface {
	TransitionInit(transition *Transition)
	TransitionStart(transition *Transition)
	TransitionEnd(transition *Transition)
	HandlerStart(transition *Transition, emitter string, handler string)
	HandlerEnd(transition *Transition, emitter string, handler string)
	// MachineInit is called only for machines with tracers added via
	// Opts.Tracers.
	MachineInit(machine Api) context.Context
	MachineDispose(machID string)
	NewSubmachine(parent, machine Api)
	Inheritable() bool
	QueueEnd(machine Api)
	SchemaChange(machine Api, old Schema)
	VerifyStates(machine Api)
}

Tracer is an interface for logging machine transitions and events, used by Opts.Tracers and Machine.BindTracer.

type Transition

type Transition struct {
	// ID is a unique identifier of the transition.
	// TODO refac to Id() with the new dbg protocol
	ID string
	// Steps is a list of steps taken by this transition (so far).
	Steps []*Step
	// TimeBefore is the machine time from before the transition.
	TimeBefore Time
	// TimeAfter is the machine time from after the transition. If the transition
	// has been canceled, this will be the same as TimeBefore. This field is nil
	// until the negotiation phase finishes.
	TimeAfter Time
	// TargetIndexes is a list of indexes of the target states.
	TargetIndexes []int
	// Enters is a list of states with enter handlers in this transition.
	Enters S
	// Enters is a list of states with exit handlers in this transition.
	Exits S
	// Mutation call which caused this transition
	Mutation *Mutation
	// Machine is the parent machine of this transition.
	Machine *Machine
	// Api is a subset of Machine.
	// TODO call when applicable instead of calling Machine
	// TODO rename to MachApi
	Api Api
	// LogEntries are log msgs produced during the transition.
	LogEntries []*LogEntry
	// PreLogEntries are log msgs produced before during the transition.
	PreLogEntries []*LogEntry
	// QueueLen is the length of the queue after the transition.
	QueueLen int
	// LogEntriesLock is used to lock the logs to be collected by a Tracer.
	LogEntriesLock sync.Mutex
	// IsCompleted returns true when the execution of the transition has been
	// fully completed.
	IsCompleted atomic.Bool
	// IsAccepted returns true if the transition has been accepted, which can
	// change during the transition's negotiation phase and while resolving
	// relations.
	IsAccepted atomic.Bool
	// contains filtered or unexported fields
}

Transition represents processing of a single mutation within a machine.

func (*Transition) Args

func (t *Transition) Args() A

Args returns the argument map passed to the mutation method (or an empty one).

func (*Transition) CalledStates

func (t *Transition) CalledStates() S

CalledStates return explicitly called / requested states of the transition.

func (*Transition) ClockAfter added in v0.7.0

func (t *Transition) ClockAfter() Clock

ClockAfter return the Clock from before the transition.

func (*Transition) ClockBefore added in v0.7.0

func (t *Transition) ClockBefore() Clock

ClockBefore return the Clock from before the transition.

func (*Transition) IsAuto

func (t *Transition) IsAuto() bool

IsAuto returns true if the transition was triggered by an auto state. Thus, it cant trigger any other auto state mutations.

func (*Transition) LogArgs added in v0.5.0

func (t *Transition) LogArgs() string

LogArgs returns a text snippet with arguments which should be logged for this Mutation.

func (*Transition) StatesBefore

func (t *Transition) StatesBefore() S

StatesBefore is a list of states before the transition.

func (*Transition) String

func (t *Transition) String() string

String representation of the transition and the steps taken so far.

func (*Transition) TargetStates

func (t *Transition) TargetStates() S

TargetStates is a list of states after parsing the relations.

func (*Transition) Type

func (t *Transition) Type() MutationType

Type returns the type of the mutation (add, remove, set).

type WhenArgsBinding added in v0.7.0

type WhenArgsBinding struct {
	// contains filtered or unexported fields
}

type WhenBinding added in v0.7.0

type WhenBinding struct {
	Ch chan struct{}
	// means states are required to NOT be active
	Negation bool
	States   StateIsActive
	Matched  int
	Total    int
}

type WhenTimeBinding added in v0.7.0

type WhenTimeBinding struct {
	Ch chan struct{}
	// map of completed to their index positions
	// TODO optimize indexes
	Index map[string]int
	// number of matches so far
	Matched int
	// number of total matches needed
	Total int
	// optional Time to match for completed from Index
	Times     Time
	Completed StateIsActive
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL