package machineimport ()// ///// ///// /////// ///// PUB UTILS// ///// ///// /////// IsActiveTick returns true if the tick represents an active state// (odd number).func ( uint64) bool {return %2 == 1}// NextActive returns the absolute tick when the state will be active again// (excluding the currently active tick).func ( uint64) uint64 {ifIsActiveTick() {return + 2 }return + 1}// NextInactive returns the absolute tick when the state will be inactive again// (excluding the currently inactive tick).func ( uint64) uint64 {if !IsActiveTick() {return + 2 }return + 1}// NextActiveIn returns the number of ticks until the state will be active again// (excluding the currently active tick).func ( uint64) int {ifIsActiveTick() {return2 }return1}// NextInactiveIn returns the number of ticks until the state will be inactive// again (excluding the currently inactive tick).func ( uint64) int {if !IsActiveTick() {return2 }return1}// IsQueued returns true if the mutation has been queued, and the result// represents the queue time it will be processed.func ( Result) bool {return > Canceled}// EnvLogLevel returns a log level from an environment variable, AM_LOG by// default.func ( string) LogLevel {if == "" { = EnvAmLog } , := strconv.Atoi(os.Getenv())returnLogLevel()}// TODO prevent using these names as state namesvar handlerSuffixes = []string{SuffixEnter, SuffixExit, SuffixState, SuffixEnd, StateAny,}// IsHandler checks if a method name is a handler method, by returning a state// name.func ( S, string) (string, string) {if == StateAny+SuffixEnter || == StateAny+SuffixState {return"", "" }// suffixesfor , := rangehandlerSuffixes {ifstrings.HasSuffix(, ) && len() != len() && != StateAny+ {return [0 : len()-len()], "" } }// AnyFooifstrings.HasPrefix(, StateAny) && len() != len(StateAny) && != StateAny+SuffixState {return [len(StateAny):], "" }// FooBarfor , := range {if !strings.HasPrefix(, ) {continue }for , := range {if + == {return , } } }return"", ""}// TestMockClock mocks the internal clock of the machine. Only for testing.func ( *Machine, Clock) { .clock = }// AMerge merges 2 or more maps into 1. Useful for passing args from many// packages.func [ comparable, any]( ...map[]) map[] { := map[]{}for , := range {for , := range { [] = } }return}// ///// ///// /////// ///// STATE LIST// ///// ///// /////// S (state names) is a string list of state names.typeS []string// FilterIndex returns a subset of S from specified indexes.func ( S) ( []int) S {returnIndexToStates(, )}// FilterPrefix returns all the states with a prefix.func ( S) ( ...string) S { := S{}for , := range { = .Add(StatesWithPrefix(, )) }return}// FilterMatch returns all the states matching a regexp.func ( S) ( *regexp.Regexp) S { := S{}for , := range {if .MatchString() { = append(, ) } }return}// Prefix lists all states with a prefix.func ( S) ( string) S {returnStatesPrefix(, )}// Add 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 ( S) ( ...S) S {iflen() == 0 {return } = append([]S{}, ...)returnslicesUniq(slices.Concat(...))}// Add1 is like [S.Add], but for single state names.func ( S) ( ...string) S { := slices.Clone() := append(, ...)returnslicesUniq()}// Delete removes states from all passed state groups.func ( S) ( ...S) S {returnSRem(, ...)}// Delete1 is like [S.Delete], but for single state names.func ( S) ( ...string) S {returnSRem(, )}// Sub returns the states from [s] that are missing in [other] (subtract).func ( S) ( S) S {returnStatesDiff(, )}// Shared returns states present in both S and other (overlap).func ( S) ( S) S {returnStatesShared(, )}// Equal returns true if states1 and states2 are equal, regardless of// order.func ( S) ( S) bool {returnStatesEqual(, )}// EqualOrder is like [S.Equal], but also checks the order of states.func ( S) ( S) bool {iflen() != len() {returnfalse }for := range {if [] != [] {returnfalse } }returntrue}// Index returns an index of passed [states]. Unknown states are// represented by -1.func ( S) ( S) []int {returnStatesToIndex(, )}func ( S) () string {returnHash(strings.Join(, ","), 4)}// ----- old// IndexToStates is deprecated, use [S.FilterIndex].func ( S, []int) S { := make(S, len())for := range { := "unknown" + strconv.Itoa([])iflen() > [] && [] != -1 { = [[]] } [] = }return}// StatesToIndex is deprecated, use [S.Index].func ( S, S) []int { := make([]int, len())for := range { [] = slices.Index(, []) }return}// StatesDiff is deprecated, use [S.Sub].func ( S, S) S {// TODO optimizereturnslicesFilter(, func( string, int) bool {return !slices.Contains(, ) })}// StatesShared is deprecated, use [S.Shared].func ( S, S) S {returnslicesFilter(, func( string, int) bool {returnslices.Contains(, ) })}// StatesEqual is deprecated, use [S.Equal].func ( S, S) bool {returnslicesEvery(, ) && slicesEvery(, )}// StatesPrefix is deprecated, use [S.Prefix].func ( string, S) S { := make(S, len())for , := range { [] = + }return}// StatesWithPrefix is deprecated, use [S.FilterPrefix].func ( string, S) S { := S{}for , := range {ifstrings.HasPrefix(, ) { = append(, ) } }return}// SAdd is deprecated, use [S.Add].func ( ...S) S {// TODO test // TODO move to resolveriflen() == 0 {returnS{} } := slices.Clone([0])for := 1; < len(); ++ { = append(, []...) }returnslicesUniq()}// SRem is deprecated, use [S.Delete].func ( S, ...S) S {// TODO test // TODO move to resolver := slices.Clone()iflen() == 0 {return }for := 1; < len(); ++ {for := 0; < len([]); ++ { = slicesWithout(, [][]) } }return}// ///// ///// /////n// ///// SCHEMA// ///// ///// /////// ----- STATE// State defines a single state of a machine, its properties, relations, and tags.// nolint:llltypeStatestruct { Auto bool`json:"auto,omitempty" yaml:"auto,omitempty" toml:"auto,omitempty"` Multi bool`json:"multi,omitempty" yaml:"multi,omitempty" toml:"multi,omitempty"` Require S`json:"require,omitempty" yaml:"require,omitempty" toml:"require,omitempty"` Add S`json:"add,omitempty" yaml:"add,omitempty" toml:"add,omitempty"` Remove S`json:"remove,omitempty" yaml:"remove,omitempty" toml:"remove,omitempty"` After S`json:"after,omitempty" yaml:"after,omitempty" toml:"after,omitempty"` Tags []string`json:"tags,omitempty" yaml:"tags,omitempty" toml:"tags,omitempty"`}// Extend 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].//// ssS.HandshakeDone: SharedSchema[ssS.HandshakeDone].Extend(// am.State{// Require: S{ssS.ClientConnected},// Remove: S{Exception},// }),func ( State) ( State) State {returnStateAdd(, )}// Set 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.//// ssS.HandshakeDone: s.Set(false, true, State{// Remove: S{"C"},// })func ( State) (, bool, State) State {returnStateSet(, , , )}// SetRels is like [State.Set], but inherits properties (Auto, Multi).func ( State) ( State) State {returnStateSet(, .Auto, .Multi, )}func ( State) () State { := State{Auto: .Auto,Multi: .Multi, }if .Require != nil { .Require = slices.Clone(.Require) }if .Add != nil { .Add = slices.Clone(.Add) }if .Remove != nil { .Remove = slices.Clone(.Remove) }if .After != nil { .After = slices.Clone(.After) }if .Tags != nil { .Tags = slices.Clone(.Tags) }return}func ( State) ( string) bool {returnslices.Contains(.Tags, )}// ----- old// StateAdd is deprecated, use [State.Extend].func ( State, State) State {// TODO example // TODO test := .Clone() := .Clone()if .Auto { .Auto = true }if .Multi { .Multi = true }// relationsif .Add != nil { .Add = SAdd(.Add, .Add) }if .Remove != nil { .Remove = SAdd(.Remove, .Remove) }if .Require != nil { .Require = SAdd(.Require, .Require) }if .After != nil { .After = SAdd(.After, .After) }return}// StateSet is Deprecated, use [State.Set].func ( State, , bool, State) State {// TODO example // TODO test := .Clone() := .Clone()// properties .Auto = .Multi = // relationsif .Add != nil { .Add = .Add }if .Remove != nil { .Remove = .Remove }if .Require != nil { .Require = .Require }if .After != nil { .After = .After }return}// ----- SCHEMA// Schema is a map of state names to state definitions.typeSchemamap[string]State// Merge merges multiple state structs into one, overriding the previous// state definitions. No relation-level merging takes place.func ( Schema) ( ...Schema) Schema {// TODO mark all-but-last states as Inherited? // TODO example // TODO test // defaults := len()switch {case0:return } := .Clone()for := 0; < ; ++ {maps.Copy(, []) }return .Clone()}// Prefix will prefix all state names with [prefix]. removeDups will skip// overlaps eg "FooFooName" will be "Foo".func ( Schema) (string, bool, , S,) Schema { := Schema{}for , := range { = .Clone()iflen() > 0 && !slices.Contains(, ) {continue } elseiflen() > 0 && slices.Contains(, ) {continue }for , := range .After { := if ! || !strings.HasPrefix(, ) { = + } .After[] = }for , := range .Add { := if ! || !strings.HasPrefix(, ) { = + } .Add[] = }for , := range .Remove { := if ! || !strings.HasPrefix(, ) { = + } .Remove[] = }for , := range .Require { := if ! || !strings.HasPrefix(, ) { = + } .Require[] = } := if ! || !strings.HasPrefix(, ) { = + }// build [] = }return}// Clone deep clones the states struct and returns a copy.func ( Schema) () Schema { := make(Schema)for , := range { [] = .Clone() }return}// Parse sanitizes the schema and returns potential errors.func ( Schema) () (Schema, error) {// TODO capitalize states // TODO ErrFoo must require Exception := .Clone() := slices.Collect(maps.Keys())varerrorfor , := range {// avoid self removalifslices.Contains(.Remove, ) { .Remove = slicesWithout(.Remove, ) }// don't Remove if in Addfor , := range .Add {ifslices.Contains(.Remove, ) { .Remove = slicesWithout(.Remove, )// check if exists } elseif !slices.Contains(, ) { .Add = slicesWithout(.Add, ) } }// avoid being after itselfifslices.Contains(.After, ) { .After = slicesWithout(.After, ) }// detect require-remove conflictsfor , := range .Require {ifslices.Contains(.Remove, ) { = errors.Join(, fmt.Errorf("%w: require-remove conflict for %s to %s",ErrSchema, , , )) } }// remove references to non-existing statesfor , := range .Remove {if !slices.Contains(, ) { .Remove = slicesWithout(.Remove, ) } }for , := range .After {if !slices.Contains(, ) { .After = slicesWithout(.After, ) } }for , := range .Remove {if !slices.Contains(, ) { .Remove = slicesWithout(.Remove, ) } } [] = }return , }// FilterByTag returns a subset schema with states having the given tag.func ( Schema) ( string) Schema { := Schema{}for , := range {ifslices.Contains(.Tags, ) { [] = } }return}// Names is a random-order list of defined state names.func ( Schema) () S {returnslices.Collect(maps.Keys())}// ----- old// SchemaMerge is deprecated, use [Schema.Merge].func ( ...Schema) Schema { := len()switch {case0:returnSchema{}case1:return [0] } := make(Schema)for := 0; < ; ++ {// TODO clone?maps.Copy(, []) }return .Clone()}// ///// ///// /////// ///// HANDLERS// ///// ///// /////// handler represents a list of transition handler methods.type handler struct { id string num int// map-based handler isMap bool mx sync.Mutex opts *BindOpts// maps negotiations map[string]HandlerNegotiation finals map[string]HandlerFinal// reflect h any methods *reflect.Value methodCache map[string]reflect.Value missingCache map[string]struct{}}// BindOpts define how the methods are bound to the state machine. Optional.typeBindOptsstruct {// Id of this binding, optional. Id string// method name will have this prefix removed MethodPrefixTrim string// only states with this prefix StatePrefix string}type handlerCall struct { fn *reflect.Value// TODO debug only name string negotiation HandlerNegotiation final HandlerFinal event *Event timeout bool}func ( *handlerCall) () bool { := trueif .fn != nil { := .fn.Call([]reflect.Value{reflect.ValueOf(.event)})iflen() > 0 {// TODO log err? , _ = [0].Interface().(bool) } } elseif .negotiation != nil { = .negotiation(.event) } elseif .final != nil { .final(.event) } else {panic("invalid handler call " + .name) }return}func newHandlerCallStruct( *Event, *handler, string, *Machine,) *handlerCall {//// cache , := .missingCache[]if { .mx.Unlock()returnnil } , := .methodCache[]if ! { = .methods.MethodByName()// support field handlersif !.IsValid() { = .methods.Elem().FieldByName() }if !.IsValid() { .missingCache[] = struct{}{} .mx.Unlock()returnnil } .methodCache[] = } .mx.Unlock()// call the handler .log(LogOps, "[handler:%d] %s", .num, ) .currentHandler.Store()// tracers .tracersMx.RLock()for := range .tracers { .tracers[].HandlerStart(.t.Load(), .id, ) } .tracersMx.RUnlock()return &handlerCall{fn: &,name: ,event: ,timeout: false, }}func newHandlerCallMap( *Event, *handler, string, *Machine,) *handlerCall {// := strings.HasSuffix(, SuffixState) ||strings.HasSuffix(, SuffixEnd)if {if , := .finals[]; ! { .mx.Unlock()returnnil } } else {if , := .negotiations[]; ! { .mx.Unlock()returnnil } } .mx.Unlock()// call the handler .log(LogOps, "[handler:%d] %s", .num, ) .currentHandler.Store()// tracers .tracersMx.RLock()for := range .tracers { .tracers[].HandlerStart(.t.Load(), .id, ) } .tracersMx.RUnlock() := &handlerCall{name: ,event: ,timeout: false, }if { .final = .finals[] } else { .negotiation = .negotiations[] }return}// ///// ///// /////// ///// UTILS// ///// ///// /////// truncateStr with shorten the string and leave a tripedot suffix.func truncateStr( string, int) string {iflen() <= {return }if < 5 {return [:] } else {return [:-3] + "..." }}// j joins state names into a single stringfunc j( []string) string {returnstrings.Join(, " ")}// jw joins state names into a single string with a separator.func jw( []string, string) string {returnstrings.Join(, )}func closeSafe[ any]( chan ) {select {case<-:default:close() }}// compareArgs return true if args2 is a subset of args1.func compareArgs(, A) bool { := truefor , := range {// TODO better comparisonsif [] != { = falsebreak } }return}// RandId generates a random ID of the given length (defaults to 8).func randId( int) string {if == 0 { = 16 } ++ = / 2 := make([]byte, ) , := rand.Read()if != nil {return"error" }returnhex.EncodeToString()}func slicesWithout[ ~[], comparable]( , ) { := slices.Index(, ) := slices.Clone()if == -1 {return }returnslices.Delete(, , +1)}// slicesNone returns true if none of the elements of coll2 are in coll1.func slicesNone[ ~[], ~[], comparable]( , ) bool {for , := range {ifslices.Contains(, ) {returnfalse } }returntrue}// slicesEvery returns true if all elements of coll2 are in coll1.func slicesEvery[ ~[], ~[], comparable]( , ) bool {for , := range {if !slices.Contains(, ) {returnfalse } }returntrue}// TODO replace with slices.DeleteFuncfunc slicesFilter[ ~[], any]( , func( , int) bool) { := make(, 0, len())for , := range {if (, ) { = append(, ) } }return}func slicesReverse[ ~[], any]( ) { := make(, len())for := range { [] = [len()-1-] }return}func slicesUniq[ comparable]( []) [] {iflen() == 0 {return []{} } := make(map[]struct{}, len()) := make([], 0, len())for , := range {if , := []; ! { [] = struct{}{} = append(, ) } }return}func cloneOptions( *Opts) *Opts {if == nil {return &Opts{} }return &Opts{Id: .Id,HandlerTimeout: .HandlerTimeout,DontPanicToException: .DontPanicToException,DontLogStackTrace: .DontLogStackTrace,DontLogId: .DontLogId,Resolver: .Resolver,LogLevel: .LogLevel,Tracers: .Tracers,LogArgs: .LogArgs,QueueLimit: .QueueLimit,Parent: .Parent,ParentId: .ParentId,Tags: .Tags,DetectEval: .DetectEval, }}func ( string) string {if == "" {return"" }// Decode the first character to find out exactly how many bytes it uses , := utf8.DecodeRuneInString()// Uppercase the first character, then append the rest of the original stringreturnstrings.ToUpper(string()) + [:]}// OptArgs will return the first [A] from a list.func ( []A) A {iflen() > 0 {return [0] }returnnil}// OptCtx will return the first [context.Context] from a list.func ( []context.Context) context.Context {iflen() > 0 {return [0] }returnnil}// OptEv will return the first [*Event] from a list.func ( []*Event) *Event {iflen() > 0 {return [0] }returnnil}// optBindOpts will return the first [BindOpts] from a list.func optBindOpts( []BindOpts) BindOpts {iflen() > 0 {return [0] }returnBindOpts{}}// goroutineNum returns the ID of the current goroutine, or 0 if err.// Numbers are re-assigned, so this is not a bulletproof way of IDing threads.func goroutineNum() int64 { := make([]byte, 4024) := runtime.Stack(, false) := string([:]) := strings.Split(, "\n") := strings.Split([0], " ") , := strconv.Atoi([1])returnint64()}func captureStackTrace() string { := make([]byte, 4024) := runtime.Stack(, false) := string([:]) := strings.Split(, "\n") := strings.Contains(, "panic")slices.Reverse() := []string{"AddErr", "AddErrState", "Remove", "Remove1", "Add", "Add1", "Set", }// TODO trim tails start at reflect.Value.Call({ // with asyncmachine 2 frames down// trim the head, remove junk := falsefor , := range {if && strings.HasPrefix(, "panic(") { = [:-1]break }for , := range {ifstrings.Contains("machine.(*Machine)."++"(", ) { = [:-1] = truebreak } }if {break } }slices.Reverse() := strings.Join(, "\n")if := os.Getenv(EnvAmTraceFilter); != "" { = strings.ReplaceAll(, , "") }return}// Hash is a general hashing function.func ( string, int) string { := md5.New() .Write([]byte())if == 0 { = 6 } := hex.EncodeToString(.Sum(nil))// short hashreturn [:]}
The pages are generated with Goldsv0.8.4. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.