package helpers
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"log/slog"
"maps"
"os"
"reflect"
"runtime"
"slices"
"sort"
"strconv"
"strings"
"time"
"github.com/alitto/pond/v2"
"github.com/failsafe-go/failsafe-go"
"github.com/failsafe-go/failsafe-go/retrypolicy"
"github.com/pancsta/asyncmachine-go/internal/utils"
am "github.com/pancsta/asyncmachine-go/pkg/machine"
ssam "github.com/pancsta/asyncmachine-go/pkg/states"
ampipe "github.com/pancsta/asyncmachine-go/pkg/states/pipes"
"github.com/pancsta/asyncmachine-go/pkg/telemetry/dbg"
)
const (
EnvAmLogPrint = "AM_LOG_PRINT"
EnvAmHealthcheck = "AM_HEALTHCHECK"
EnvAmTestRunner = "AM_TEST_RUNNER"
EnvAmLogFull = "AM_LOG_FULL"
EnvAmLogSteps = "AM_LOG_STEPS"
EnvAmLogGraph = "AM_LOG_GRAPH"
EnvAmLogChecks = "AM_LOG_CHECKS"
EnvAmLogQueued = "AM_LOG_QUEUED"
EnvAmLogArgs = "AM_LOG_ARGS"
EnvAmLogWhen = "AM_LOG_WHEN"
EnvAmLogStateCtx = "AM_LOG_STATE_CTX"
EnvAmLogFile = "AM_LOG_FILE"
healthcheckInterval = 30 * time .Second
)
type (
S = am .S
A = am .A
Schema = am .Schema
)
func Add1Sync (
ctx context .Context , mach am .Api , state string , args ...am .A ,
) bool {
return EvAddSync (ctx , nil , mach , S {state }, args ...)
}
func AddSync (
ctx context .Context , mach am .Api , states S , args ...am .A ,
) bool {
return EvAddSync (ctx , nil , mach , states , args ...)
}
func EvAdd1Sync (
ctx context .Context , e *am .Event , mach am .Api , state string , args ...am .A ,
) bool {
return EvAddSync (ctx , e , mach , S {state }, args ...)
}
func EvAddSync (
ctx context .Context , e *am .Event , mach am .Api , states S , args ...am .A ,
) bool {
res := mach .EvAdd (e , states , am .OptArgs (args ))
switch res {
case am .Executed :
return true
case am .Canceled :
return false
default :
select {
case <- ctx .Done ():
return false
case <- mach .WhenQueue (res ):
if mach .Is (states ) {
return true
}
return false
}
}
}
func Add1Async (
ctx context .Context , mach am .Api , waitState string ,
addState string , args ...am .A ,
) bool {
return EvAddAsync (ctx , nil , mach , waitState , S {addState }, am .OptArgs (args ))
}
func AddAsync (
ctx context .Context , mach am .Api , waitState string ,
addStates S , args ...am .A ,
) bool {
return EvAddAsync (ctx , nil , mach , waitState , addStates , am .OptArgs (args ))
}
func EvAdd1Async (
ctx context .Context , e *am .Event , mach am .Api , waitState string ,
addState string , args ...am .A ,
) bool {
return EvAddAsync (ctx , e , mach , waitState , S {addState }, am .OptArgs (args ))
}
func EvAddAsync (
ctx context .Context , e *am .Event , mach am .Api , waitState string ,
addStates S , args ...am .A ,
) bool {
ctxWhen , cancel := context .WithCancel (ctx )
defer cancel ()
tickBefore := mach .Tick (waitState )
ticks := am .NextActiveIn (tickBefore )
when := mach .WhenTicks (waitState , ticks , ctxWhen )
if mach .EvAdd (e , addStates , am .OptArgs (args )) == am .Canceled {
return false
}
select {
case <- when :
return true
case <- ctx .Done ():
return false
}
}
func Remove1Sync (
ctx context .Context , mach am .Api , state string , args ...am .A ,
) bool {
return EvRemoveSync (ctx , nil , mach , S {state }, args ...)
}
func RemoveSync (
ctx context .Context , mach am .Api , states S , args ...am .A ,
) bool {
return EvRemoveSync (ctx , nil , mach , states , args ...)
}
func EvRemove1Sync (
ctx context .Context , e *am .Event , mach am .Api , state string , args ...am .A ,
) bool {
return EvRemoveSync (ctx , e , mach , S {state }, args ...)
}
func EvRemoveSync (
ctx context .Context , e *am .Event , mach am .Api , states S , args ...am .A ,
) bool {
res := mach .EvRemove (e , states , am .OptArgs (args ))
switch res {
case am .Executed :
return true
case am .Canceled :
return false
default :
select {
case <- ctx .Done ():
return false
case <- mach .WhenQueue (res ):
if mach .Not (states ) {
return true
}
return true
}
}
}
func IsMulti (mach am .Api , state string ) bool {
return mach .Schema ()[state ].Multi
}
func StatesToIndexes (allStates am .S , states am .S ) []int {
indexes := make ([]int , len (states ))
for i , state := range states {
indexes [i ] = slices .Index (allStates , state )
}
return indexes
}
func IndexesToStates (allStates am .S , indexes []int ) am .S {
states := make (am .S , len (indexes ))
for i , idx := range indexes {
if idx == -1 || idx >= len (allStates ) {
states [i ] = "unknown" + strconv .Itoa (i )
continue
}
states [i ] = allStates [idx ]
}
return states
}
func MachDebug (
mach am .Api , amDbgAddr string , logLvl am .LogLevel , stdout bool ,
semConfig *am .SemConfig ,
) error {
if amDbgAddr == "1" {
amDbgAddr = dbg .DbgAddr
}
if IsTestRunner () {
return nil
}
err , done := semLogInit (mach .SemLogger (), amDbgAddr , logLvl , stdout ,
semConfig )
if done {
return err
}
err = dbg .TransitionsToDbg (mach , amDbgAddr )
if err != nil {
return err
}
if os .Getenv (EnvAmHealthcheck ) != "" {
Healthcheck (mach )
}
return nil
}
func MachDebugWs (
mach am .Api , amDbgAddr string , logLvl am .LogLevel , stdout bool ,
semConfig *am .SemConfig ,
) error {
if amDbgAddr == "1" {
amDbgAddr = dbg .DbgAddrWeb
}
if IsTestRunner () {
return nil
}
err , done := semLogInit (mach .SemLogger (), amDbgAddr , logLvl , stdout ,
semConfig )
if done {
return err
}
err = dbg .TransitionsToDbg (mach , amDbgAddr , &dbg .Opts {
WebSocket : true ,
})
if err != nil {
return err
}
if os .Getenv (EnvAmHealthcheck ) != "" {
Healthcheck (mach )
}
return nil
}
func semLogInit(
semlog am .SemLogger , amDbgAddr string , logLvl am .LogLevel , stdout bool ,
semConfig *am .SemConfig ,
) (error , bool ) {
if stdout {
semlog .SetLevel (logLvl )
} else {
semlog .SetEmpty (logLvl )
}
if amDbgAddr == "" {
return nil , true
}
if semConfig .Steps {
semlog .EnableSteps (true )
}
if semConfig .Graph {
semlog .EnableGraph (true )
}
if semConfig .Can {
semlog .EnableCan (true )
}
if semConfig .Queued {
semlog .EnableQueued (true )
}
if semConfig .StateCtx {
semlog .EnableStateCtx (true )
}
if semConfig .Can {
semlog .EnableCan (true )
}
if semConfig .When {
semlog .EnableWhen (true )
}
if semConfig .Args {
semlog .EnableArgs (true )
}
return nil , false
}
func SemConfigEnv (forceFull bool ) *am .SemConfig {
if os .Getenv (EnvAmLogFull ) != "" || forceFull {
return &am .SemConfig {
Steps : true ,
Graph : true ,
Can : true ,
Queued : true ,
StateCtx : true ,
When : true ,
Args : true ,
}
}
return &am .SemConfig {
Steps : os .Getenv (EnvAmLogSteps ) != "" ,
Graph : os .Getenv (EnvAmLogGraph ) != "" ,
Can : os .Getenv (EnvAmLogChecks ) != "" ,
Queued : os .Getenv (EnvAmLogQueued ) != "" ,
StateCtx : os .Getenv (EnvAmLogStateCtx ) != "" ,
When : os .Getenv (EnvAmLogWhen ) != "" ,
Args : os .Getenv (EnvAmLogArgs ) != "" ,
}
}
func MachDebugEnv (mach am .Api ) error {
amDbgAddr := os .Getenv (dbg .EnvAmDbgAddr )
logLvl := am .EnvLogLevel ("" )
stdout := os .Getenv (EnvAmLogPrint ) != ""
if IsWasm () {
return MachDebugWs (mach , amDbgAddr , logLvl , stdout , SemConfigEnv (false ))
}
return MachDebug (mach , amDbgAddr , logLvl , stdout , SemConfigEnv (false ))
}
func Healthcheck (mach am .Api ) {
if !mach .Has1 (ssam .BasicStates .Healthcheck ) {
return
}
go func () {
for {
if !Wait (mach .Context (), healthcheckInterval ) {
break
}
mach .Add1 (ssam .BasicStates .Healthcheck , nil )
}
}()
}
func NewReqAdd (mach am .Api , states am .S , args am .A ) *MutRequest {
return NewMutRequest (mach , am .MutationAdd , states , args )
}
func NewReqAdd1 (mach am .Api , state string , args am .A ) *MutRequest {
return NewReqAdd (mach , am .S {state }, args )
}
func NewReqRemove (mach am .Api , states am .S , args am .A ) *MutRequest {
return NewMutRequest (mach , am .MutationRemove , states , args )
}
func NewReqRemove1 (mach am .Api , state string , args am .A ) *MutRequest {
return NewReqRemove (mach , am .S {state }, args )
}
type MutRequest struct {
Mach am .Api
MutType am .MutationType
States am .S
Args am .A
Event *am .Event
PolicyRetries int
PolicyDelay time .Duration
PolicyBackoff time .Duration
PolicyMaxDuration time .Duration
}
func NewMutRequest (
mach am .Api , mutType am .MutationType , states am .S , args am .A ,
) *MutRequest {
return &MutRequest {
Mach : mach ,
MutType : mutType ,
States : states ,
Args : args ,
PolicyRetries : 10 ,
PolicyDelay : 100 * time .Millisecond ,
PolicyBackoff : 5 * time .Second ,
PolicyMaxDuration : 5 * time .Second ,
}
}
func (r *MutRequest ) Clone (
mach am .Api , mutType am .MutationType , states am .S , args am .A ,
) *MutRequest {
return &MutRequest {
Mach : mach ,
MutType : mutType ,
States : states ,
Args : args ,
PolicyRetries : r .PolicyRetries ,
PolicyBackoff : r .PolicyBackoff ,
PolicyMaxDuration : r .PolicyMaxDuration ,
PolicyDelay : r .PolicyDelay ,
}
}
func (r *MutRequest ) Retries (retries int ) *MutRequest {
r .PolicyRetries = retries
return r
}
func (r *MutRequest ) Backoff (backoff time .Duration ) *MutRequest {
r .PolicyBackoff = backoff
return r
}
func (r *MutRequest ) MaxDuration (maxDuration time .Duration ) *MutRequest {
r .PolicyMaxDuration = maxDuration
return r
}
func (r *MutRequest ) Delay (delay time .Duration ) *MutRequest {
r .PolicyDelay = delay
return r
}
func (r *MutRequest ) Run (ctx context .Context ) (am .Result , error ) {
retry := retrypolicy .Builder [am .Result ]().
WithMaxDuration (r .PolicyMaxDuration ).
WithMaxRetries (r .PolicyRetries )
if r .PolicyBackoff != 0 {
retry = retry .WithBackoff (r .PolicyDelay , r .PolicyBackoff )
} else {
retry = retry .WithDelay (r .PolicyDelay )
}
res , err := failsafe .NewExecutor [am .Result ](retry .Build ()).WithContext (ctx ).
Get (r .get )
return res , err
}
func (r *MutRequest ) get () (am .Result , error ) {
var res am .Result
if r .MutType == am .MutationAdd {
res = r .Mach .EvAdd (r .Event , r .States , r .Args )
} else {
res = r .Mach .EvRemove (r .Event , r .States , r .Args )
}
return res , ResultToErr (res )
}
func Wait (ctx context .Context , length time .Duration ) bool {
t := time .After (length )
select {
case <- ctx .Done ():
return false
case <- t :
return true
}
}
func Interval (
ctx context .Context , length time .Duration , interval time .Duration ,
fn func () bool ,
) error {
end := time .Now ().Add (length )
t := time .NewTicker (interval )
defer t .Stop ()
for {
select {
case <- ctx .Done ():
return ctx .Err ()
case <- t .C :
if time .Now ().After (end ) {
return nil
}
if !fn () {
return nil
}
}
}
}
func WaitForAll (
ctx context .Context , timeout time .Duration , chans ...<-chan struct {},
) error {
if len (chans ) == 0 {
return nil
}
if ctx .Err () != nil {
return ctx .Err ()
}
if IsDebug () {
timeout = 100 * timeout
}
t := time .After (timeout )
for _ , ch := range chans {
select {
case <- ctx .Done ():
return ctx .Err ()
case <- t :
return am .ErrTimeout
case <- ch :
}
}
return nil
}
func WaitForErrAll (
ctx context .Context , timeout time .Duration , mach am .Api ,
chans ...<-chan struct {},
) error {
if len (chans ) == 0 {
return nil
}
if ctx .Err () != nil {
return ctx .Err ()
}
if IsDebug () {
timeout = 100 * timeout
}
t := time .After (timeout )
whenErr := mach .WhenErr (ctx )
for _ , ch := range chans {
select {
case <- ctx .Done ():
return ctx .Err ()
case <- whenErr :
return fmt .Errorf ("%s: %w" , am .StateException , mach .Err ())
case <- t :
return am .ErrTimeout
case <- ch :
}
}
return nil
}
func WaitForAny (
ctx context .Context , timeout time .Duration , chans ...<-chan struct {},
) error {
if ctx .Err () != nil {
return ctx .Err ()
}
if IsDebug () {
timeout = 100 * timeout
}
t := time .After (timeout )
cases := make ([]reflect .SelectCase , 2 +len (chans ))
cases [0 ] = reflect .SelectCase {
Dir : reflect .SelectRecv ,
Chan : reflect .ValueOf (ctx .Done ()),
}
cases [1 ] = reflect .SelectCase {
Dir : reflect .SelectRecv ,
Chan : reflect .ValueOf (t ),
}
for i , ch := range chans {
cases [i +2 ] = reflect .SelectCase {
Dir : reflect .SelectRecv ,
Chan : reflect .ValueOf (ch ),
}
}
chosen , _ , _ := reflect .Select (cases )
switch chosen {
case 0 :
return ctx .Err ()
case 1 :
return am .ErrTimeout
default :
return nil
}
}
func WaitForErrAny (
ctx context .Context , timeout time .Duration , mach *am .Machine ,
chans ...<-chan struct {},
) error {
if ctx .Err () != nil {
return ctx .Err ()
}
if IsDebug () {
timeout = 100 * timeout
}
t := time .After (timeout )
predef := 3
cases := make ([]reflect .SelectCase , predef +len (chans ))
cases [0 ] = reflect .SelectCase {
Dir : reflect .SelectRecv ,
Chan : reflect .ValueOf (ctx .Done ()),
}
cases [1 ] = reflect .SelectCase {
Dir : reflect .SelectRecv ,
Chan : reflect .ValueOf (t ),
}
cases [2 ] = reflect .SelectCase {
Dir : reflect .SelectRecv ,
Chan : reflect .ValueOf (t ),
}
for i , ch := range chans {
cases [predef +i ] = reflect .SelectCase {
Dir : reflect .SelectRecv ,
Chan : reflect .ValueOf (ch ),
}
}
chosen , _ , _ := reflect .Select (cases )
switch chosen {
case 0 :
return ctx .Err ()
case 1 :
return am .ErrTimeout
case 2 :
return mach .Err ()
default :
return nil
}
}
func Activations (u uint64 ) int {
return int ((u + 1 ) / 2 )
}
func ExecAndClose (fn func ()) <-chan struct {} {
ch := make (chan struct {})
go func () {
fn ()
close (ch )
}()
return ch
}
func EnableDebugging (stdout bool ) {
if os .Getenv (am .EnvAmTestRunner ) != "" {
return
}
_ = os .Setenv (am .EnvAmDebug , "1" )
if stdout {
_ = os .Setenv (EnvAmLogPrint , "1" )
}
_ = os .Setenv (dbg .EnvAmDbgAddr , "1" )
_ = os .Setenv (EnvAmLogFull , "1" )
SetEnvLogLevel (am .LogOps )
}
func SetEnvLogLevel (level am .LogLevel ) {
_ = os .Setenv (am .EnvAmLog , strconv .Itoa (int (level )))
}
func Implements (statesChecked , statesNeeded am .S ) error {
for _ , state := range statesNeeded {
if !slices .Contains (statesChecked , state ) {
return errors .New ("missing state: " + state )
}
}
return nil
}
func LogArgs (args am .A , maxLen int ) map [string ]string {
ret := map [string ]string {}
for _ , arg := range args {
for k , v := range ArgsToLogMap (arg , maxLen ) {
ret [k ] = v
}
}
return ret
}
func LogArgsMapper (args am .A ) map [string ]string {
return LogArgs (args , 0 )
}
func ArgsToLogMap (args any , maxLen int ) map [string ]string {
if maxLen == 0 {
maxLen = max (4 , am .LogArgsMaxLen )
}
skipMaxLen := false
result := make (map [string ]string )
val := reflect .ValueOf (args )
if val .Kind () == reflect .Ptr {
val = val .Elem ()
}
if !val .IsValid () || val .Kind () != reflect .Struct {
return result
}
typ := val .Type ()
for i := 0 ; i < val .NumField (); i ++ {
field := val .Field (i )
key := typ .Field (i ).Tag .Get ("log" )
if key == "" {
continue
}
switch v := field .Interface ().(type ) {
case string :
if v == "" {
continue
}
result [key ] = v
case []string :
if len (v ) == 0 {
continue
}
skipMaxLen = true
txt := ""
ii := 0
for _ , el := range v {
val := reflect .ValueOf (v )
if val .Kind () == reflect .Ptr && val .IsNil () {
continue
}
if txt != "" {
txt += ", "
}
txt += `"` + utils .TruncateStr (el , maxLen /2 ) + `"`
if ii >= maxLen /2 {
txt += fmt .Sprintf (" ... (%d more)" , len (v )-ii )
break
}
ii ++
}
if txt == "" {
continue
}
result [key ] = txt
case bool :
if !v {
continue
}
result [key ] = fmt .Sprintf ("%v" , v )
case []bool :
if len (v ) == 0 {
continue
}
result [key ] = fmt .Sprintf ("%v" , v )
result [key ] = strings .Trim (result [key ], "[]" )
case int :
if v == 0 {
continue
}
result [key ] = fmt .Sprintf ("%d" , v )
case []int :
if len (v ) == 0 {
continue
}
result [key ] = fmt .Sprintf ("%d" , v )
result [key ] = strings .Trim (result [key ], "[]" )
case time .Duration :
if v .Seconds () == 0 {
continue
}
result [key ] = v .String ()
case fmt .Stringer :
val := reflect .ValueOf (v )
if val .Kind () == reflect .Ptr && val .IsNil () {
continue
}
txt := v .String ()
if txt == "" {
continue
}
result [key ] = txt
default :
if field .Kind () != reflect .Slice {
continue
}
valLen := field .Len ()
skipMaxLen = true
txt := ""
ii := 0
for i := 0 ; i < valLen ; i ++ {
el := field .Index (i ).Interface ()
s , ok := el .(fmt .Stringer )
if ok && s .String () != "" {
if txt != "" {
txt += ", "
}
txt += `"` + utils .TruncateStr (s .String (), maxLen /2 ) + `"`
if i >= maxLen /2 {
txt += fmt .Sprintf (" ... (%d more)" , valLen -ii )
break
}
}
ii ++
}
if txt == "" {
continue
}
result [key ] = txt
}
result [key ] = strings .ReplaceAll (result [key ], "\n" , " " )
if !skipMaxLen && len (result [key ]) > maxLen {
result [key ] = utils .TruncateStr (result [key ], maxLen )
}
}
return result
}
type ArgsUnmarshallerFn func (args am .A ) am .A
func ArgsUnmarshal [G am .ArgsApi ](args am .A , def G ) am .A {
ret := am .A {}
coll := map [string ]string {}
for key , val := range args {
arg , ok := strings .CutPrefix (key , def .ArgsState ()+"." )
if !ok {
continue
}
valS , ok := val .(string )
if !ok {
continue
}
coll [arg ] = valS
}
clone := def
valB , err := json .Marshal (coll )
if err != nil {
return ret
}
err = json .Unmarshal (valB , &clone )
if err != nil {
return ret
}
ret [am .ArgIndex (def )] = &clone
return ret
}
func ArgsNames (args []am .ArgsApi ) ([]string , error ) {
var ret []string
for _ , arg := range args {
argB , err := json .Marshal (arg )
if err != nil {
return nil , err
}
names := map [string ]any {}
if err := json .Unmarshal (argB , &names ); err != nil {
return nil , err
}
for name := range names {
ret = append (ret , arg .ArgsState ()+"." +name )
}
}
return ret , nil
}
func NewArgsUnmarshaller (defs []am .ArgsApi ) ArgsUnmarshallerFn {
return func (args am .A ) am .A {
merge := make ([]am .A , len (defs ))
for i , def := range defs {
merge [i ] = ArgsUnmarshal (args , def )
}
return am .PassMerge (merge ...)
}
}
func IsDebug () bool {
return os .Getenv (am .EnvAmDebug ) != "" && !IsTestRunner ()
}
func IsWasm () bool {
return runtime .GOARCH == "wasm" && runtime .GOOS == "js"
}
func IsWasi () bool {
return runtime .GOARCH == "wasm" &&
(runtime .GOOS == "wasip1" || runtime .GOOS == "wasip2" )
}
func IsTelemetry () bool {
return os .Getenv (dbg .EnvAmDbgAddr ) != "" && !IsTestRunner ()
}
func IsTestRunner () bool {
return os .Getenv (EnvAmTestRunner ) != ""
}
func GroupWhen1 (
machs []am .Api , state string , ctx context .Context ,
) ([]<-chan struct {}, error ) {
for _ , mach := range machs {
if !mach .Has1 (state ) {
return nil , fmt .Errorf (
"%w: %s in machine %s" , am .ErrStateMissing , state , mach .Id (),
)
}
}
var chans []<-chan struct {}
for _ , mach := range machs {
chans = append (chans , mach .When1 (state , ctx ))
}
return chans , nil
}
func RemoveMulti (mach am .Api , state string ) am .HandlerFinal {
return func (_ *am .Event ) {
mach .Remove1 (state , nil )
}
}
func GetTransitionStates (
tx *am .Transition , index am .S ,
) (added am .S , removed am .S , touched am .S ) {
before := tx .TimeBefore
after := tx .TimeAfter
is := func (time am .Time , i int ) bool {
return time != nil && am .IsActiveTick (time .Tick (i ))
}
for i , name := range index {
if is (before , i ) && !is (after , i ) {
removed = append (removed , name )
} else if !is (before , i ) && is (after , i ) {
added = append (added , name )
} else if before != nil && before .Tick (i ) != after .Tick (i ) {
added = append (added , name )
}
}
touched = am .S {}
for _ , step := range tx .Steps {
if s := step .GetFromState (index ); s != "" {
touched = append (touched , s )
}
if s := step .GetToState (index ); s != "" {
touched = append (touched , s )
}
}
return added , removed , utils .SlicesUniq (touched )
}
func ResultToErr (result am .Result ) error {
switch result {
case am .Canceled :
return am .ErrCanceled
default :
return nil
}
}
type MachGroup []am .Api
func (g *MachGroup ) Is1 (state string ) bool {
if g == nil {
return false
}
for _ , m := range *g {
if m .Not1 (state ) {
return false
}
}
return true
}
func Pool (ctx context .Context , limit int ) pond .TaskGroup {
pool := pond .NewPool (limit )
return pool .NewGroupContext (ctx )
}
type Cond struct {
Is S
Any []S
Any1 S
Not S
Clock am .Clock
}
func (c Cond ) String () string {
return fmt .Sprintf ("is: %s, any: %s, not: %s, clock: %v" ,
c .Is , c .Any1 , c .Not , c .Clock )
}
func (c Cond ) Check (mach am .Api ) bool {
if mach == nil {
return false
}
if !mach .Is (c .Is ) {
return false
}
if mach .Any1 (c .Not ...) {
return false
}
if len (c .Any1 ) > 0 && !mach .Any1 (c .Any1 ...) {
return false
}
if len (c .Any ) > 0 && !mach .Any (c .Any ...) {
return false
}
if !mach .WasClock (c .Clock ) {
return false
}
return true
}
func (c Cond ) IsEmpty () bool {
return c .Is == nil && c .Any1 == nil && c .Not == nil && c .Clock == nil
}
type StateLoop struct {
ResetInterval time .Duration
Threshold int
loopState string
ctxStates am .S
mach am .Api
ended bool
check func () bool
lastMTime uint64
lastHTime time .Time
startMTime uint64
startHTime time .Time
}
func (l *StateLoop ) String () string {
ok := "ok"
if l .ended {
ok = "ended"
}
return fmt .Sprintf ("StateLoop: %s for %s/%s" , ok , l .mach .Id (), l .loopState )
}
func (l *StateLoop ) Break () {
l .ended = true
l .mach .Log (l .String ())
}
func (l *StateLoop ) Sum () uint64 {
return l .mach .Time (l .ctxStates ).Sum (nil )
}
func (l *StateLoop ) Ok (ctx context .Context ) bool {
if l .ended {
return false
} else if ctx != nil && ctx .Err () != nil {
err := fmt .Errorf ("loop: arg ctx expired for %s/%s" , l .mach .Id (),
l .loopState )
l .mach .AddErr (err , nil )
l .ended = true
return false
} else if l .mach .Not1 (l .loopState ) {
err := fmt .Errorf ("loop: state ctx expired for %s/%s" , l .mach .Id (),
l .loopState )
l .mach .AddErr (err , nil )
l .ended = true
return false
}
if l .check != nil && !l .check () {
l .ended = true
return false
}
sum := l .mach .Time (l .ctxStates ).Sum (nil )
if time .Since (l .lastHTime ) > l .ResetInterval {
l .lastHTime = time .Now ()
l .lastMTime = sum
return true
} else if int (sum ) > l .Threshold {
err := fmt .Errorf ("loop: threshold exceeded for %s/%s" , l .mach .Id (),
l .loopState )
l .mach .AddErr (err , nil )
l .ended = true
return false
}
l .lastMTime = sum
return true
}
func (l *StateLoop ) Ended () bool {
return l .ended
}
func NewStateLoop (
mach *am .Machine , loopState string , optCheck func () bool ,
) *StateLoop {
schema := mach .Schema ()
if !mach .Has1 (loopState ) {
return &StateLoop {ended : true }
}
ctxStates := S {loopState }
ctxStates = append (ctxStates , schema [loopState ].Require ...)
resolver := mach .Resolver ()
inbound , _ := resolver .InboundRelationsOf (loopState )
for _ , name := range inbound {
rels , _ := resolver .RelationsBetween (name , loopState )
if len (rels ) > 0 {
ctxStates = append (ctxStates , name )
}
}
l := &StateLoop {
ResetInterval : time .Second ,
Threshold : 500 ,
loopState : loopState ,
mach : mach ,
ctxStates : ctxStates ,
startHTime : time .Now (),
startMTime : mach .Time (ctxStates ).Sum (nil ),
check : optCheck ,
}
mach .Log (l .String ())
return l
}
var SlogToMachLogOpts = &slog .HandlerOptions {
ReplaceAttr : func (groups []string , a slog .Attr ) slog .Attr {
if a .Key == slog .TimeKey || a .Key == slog .LevelKey {
return slog .Attr {}
}
return a
},
}
type SlogToMachLog struct {
Mach am .Api
}
func (l SlogToMachLog ) Write (p []byte ) (n int , err error ) {
s , _ := strings .CutPrefix (string (p ), "msg=" )
l .Mach .Log (s )
return len (p ), nil
}
func MachToSlog (mach am .Api ) *slog .Logger {
return slog .New (slog .NewTextHandler (
SlogToMachLog {Mach : mach }, SlogToMachLogOpts ,
))
}
func TagValue (tags []string , key string ) string {
for _ , t := range tags {
if t == key {
return key
}
p := key + ":"
if !strings .HasPrefix (t , p ) {
continue
}
val , _ := strings .CutPrefix (t , p )
return val
}
return ""
}
func TagValueInt (tags []string , key string ) int {
v := TagValue (tags , key )
if v == "" {
return -1
}
i , _ := strconv .Atoi (v )
return i
}
func CountRelations (state *am .State ) int {
return len (state .Remove ) + len (state .Add ) + len (state .Require ) +
len (state .After )
}
func NewMirror (
id string , flat bool , source *am .Machine , handlers any , states am .S ,
) (*am .Machine , error ) {
v := reflect .ValueOf (handlers )
if v .Kind () != reflect .Ptr || v .Elem ().Kind () != reflect .Struct {
return nil , errors .New ("BindHandlers expects a pointer to a struct" )
}
vElem := v .Elem ()
var methodNames []string
methodNames , err := listHandlers (handlers , states )
if err != nil {
return nil , fmt .Errorf ("listing handlers: %w" , err )
}
if id == "" {
id = "mirror-" + source .Id ()
}
sourceSchema := source .Schema ()
names := am .S {am .StateException }
schema := am .Schema {}
for _ , name := range states {
schema [name ] = am .State {
Multi : sourceSchema [name ].Multi ,
}
names = append (names , name )
}
mirror := am .New (source .Context (), schema , &am .Opts {
Id : id ,
Parent : source ,
})
for _ , method := range methodNames {
var state string
var isAdd bool
field := vElem .FieldByName (method )
if strings .HasSuffix (method , am .SuffixState ) {
state = method [:len (method )-len (am .SuffixState )]
isAdd = true
} else if strings .HasSuffix (method , am .SuffixEnd ) {
state = method [:len (method )-len (am .SuffixEnd )]
} else {
return nil , fmt .Errorf ("unsupported handler %s for %s" , method , id )
}
var p am .HandlerFinal
if flat {
if source .Is1 (state ) {
mirror .Add1 (state , nil )
}
if isAdd {
p = ampipe .AddFlat (source , mirror , state , "" )
} else {
p = ampipe .RemoveFlat (source , mirror , state , "" )
}
} else {
if isAdd {
p = ampipe .Add (source , mirror , state , "" )
} else {
p = ampipe .Remove (source , mirror , state , "" )
}
}
field .Set (reflect .ValueOf (p ))
}
if _ , err := source .HandlersBind (handlers ); err != nil {
return nil , err
}
return mirror , nil
}
func listHandlers(handlers any , states S ) ([]string , error ) {
var methodNames []string
var errs []error
check := func (method string ) {
s1 , s2 := am .IsHandler (states , method )
if s1 != "" && !slices .Contains (states , s1 ) {
errs = append (errs , fmt .Errorf (
"%w: %s from handler %s" , am .ErrStateMissing , s1 , method ,
))
}
if s2 != "" && !slices .Contains (states , s2 ) {
errs = append (errs , fmt .Errorf (
"%w: %s from handler %s" , am .ErrStateMissing , s2 , method ,
))
}
if s1 != "" || method == am .StateAny +am .SuffixEnter ||
method == am .StateAny +am .SuffixState {
methodNames = append (methodNames , method )
}
}
t := reflect .TypeOf (handlers )
for i := 0 ; i < t .NumMethod (); i ++ {
method := t .Method (i ).Name
check (method )
}
val := reflect .ValueOf (handlers ).Elem ()
typ := val .Type ()
for i := 0 ; i < val .NumField (); i ++ {
kind := typ .Field (i ).Type .Kind ()
if kind != reflect .Func {
continue
}
method := typ .Field (i ).Name
check (method )
}
return methodNames , errors .Join (errs ...)
}
func CopySchema (source am .Schema , target *am .Machine , states am .S ) error {
if len (states ) == 0 {
return nil
}
newSchema := target .Schema ()
for _ , name := range states {
if _ , ok := source [name ]; !ok {
return fmt .Errorf ("%w: state %s in source schema" ,
am .ErrStateMissing , name )
}
newSchema [name ] = source [name ]
}
newStates := utils .SlicesUniq (slices .Concat (target .StateNames (), states ))
return target .SetSchema (newSchema , newStates )
}
func SchemaHash (schema am .Schema ) string {
ret := ""
if schema == nil {
return ""
}
keys := slices .Collect (maps .Keys (schema ))
sort .Strings (keys )
for _ , k := range keys {
ret += k + ":"
if schema [k ].Auto {
ret += "a,"
}
if schema [k ].Multi {
ret += "m,"
}
after := slices .Clone (schema [k ].After )
sort .Strings (after )
for _ , r := range after {
ret += r + ","
}
ret += ";"
remove := slices .Clone (schema [k ].Remove )
sort .Strings (remove )
for _ , r := range remove {
ret += r + ","
}
ret += ";"
add := slices .Clone (schema [k ].Add )
sort .Strings (add )
for _ , r := range add {
ret += r + ","
}
ret += ";"
require := slices .Clone (schema [k ].Require )
sort .Strings (require )
for _ , r := range require {
ret += r + ","
}
ret += ";"
}
return am .Hash (ret , 6 )
}
func EvalGetter [T any ](
ctx context .Context , source string , maxTries int , mach *am .Machine ,
eval func () (T , error ),
) (T , error ) {
var ret T
var retErr error
evalOuter := func () {
ret , retErr = eval ()
}
for range min (maxTries , 1 ) {
if !mach .Eval ("Get/" +source , evalOuter , ctx ) {
retErr = fmt .Errorf ("%w: EvalGet/%s" , am .ErrEvalTimeout , source )
} else {
break
}
}
return ret , retErr
}
func EvalSetter (
ctx context .Context , source string , maxTries int , mach *am .Machine ,
eval func () error ,
) error {
var retErr error
evalOuter := func () {
retErr = eval ()
}
for range min (maxTries , 1 ) {
if !mach .Eval ("Set/" +source , evalOuter , ctx ) {
retErr = fmt .Errorf ("%w: EvalSet/%s" , am .ErrEvalTimeout , source )
} else {
break
}
}
return retErr
}
func CantAdd (mach am .Api , states am .S , args am .A ) bool {
args2 := &am .ACheck {
CheckDone : make (chan struct {}),
}
mach .CanAdd (states , am .PassMerge (args , am .Pass (args2 )))
<-args2 .CheckDone
return !args2 .Canceled
}
func CantAdd1 (mach am .Api , state string , args am .A ) bool {
return mach .CanAdd (am .S {state }, args ) == am .Canceled
}
func CantRemove (mach am .Api , states am .S , args am .A ) bool {
args2 := &am .ACheck {
CheckDone : make (chan struct {}),
}
mach .CanRemove (states , am .PassMerge (args , am .Pass (args2 )))
<-args2 .CheckDone
return args2 .Canceled
}
func CantRemove1 (mach am .Api , state string , args am .A ) bool {
return mach .CanRemove (am .S {state }, args ) == am .Canceled
}
func AskAdd (mach am .Api , states am .S , args am .A ) am .Result {
return AskEvAdd (nil , mach , states , args )
}
func AskEvAdd (e *am .Event , mach am .Api , states am .S , args am .A ) am .Result {
if !CantAdd (mach , states , args ) {
return mach .EvAdd (e , states , args )
}
return am .Canceled
}
func AskAdd1 (mach am .Api , state string , args am .A ) am .Result {
return AskAdd (mach , S {state }, args )
}
func AskEvAdd1 (e *am .Event , mach am .Api , state string , args am .A ) am .Result {
return AskEvAdd (e , mach , S {state }, args )
}
func AskRemove (mach am .Api , states am .S , args am .A ) am .Result {
return AskEvRemove (nil , mach , states , args )
}
func AskEvRemove (e *am .Event , mach am .Api , states am .S , args am .A ) am .Result {
if !CantRemove (mach , states , args ) {
return mach .EvRemove (e , states , args )
}
return am .Canceled
}
func AskRemove1 (mach am .Api , state string , args am .A ) am .Result {
return AskRemove (mach , S {state }, args )
}
func AskEvRemove1 (e *am .Event , mach am .Api , state string , args am .A ) am .Result {
return AskEvRemove (e , mach , S {state }, args )
}
func DisposeBind (mach am .Api , handler am .HandlerDispose ) {
state := ssam .DisposedStates .RegisterDisposal
if mach .Has1 (state ) {
mach .Add1 (state , am .A {
ssam .DisposedArgHandler : handler ,
})
return
}
mach .OnDispose (handler )
}
func Dispose (mach am .Api ) {
state := ssam .DisposedStates .Disposing
if mach .Has1 (state ) {
mach .Add1 (state , nil )
return
}
mach .Dispose ()
}
func DisposeEv (mach am .Api , e *am .Event ) {
state := ssam .DisposedStates .Disposing
if mach .Has1 (state ) {
mach .EvAdd1 (e , state , nil )
return
}
mach .Dispose ()
}
func SchemaImplements (schema am .Schema , states am .S ) error {
return Implements (schema .Names (), states )
}
func HandlerToState (handler string ) string {
return strings .TrimSuffix (
strings .TrimSuffix (
strings .TrimSuffix (
strings .TrimSuffix (handler ,
am .SuffixState ), am .SuffixEnter ,
), am .SuffixEnd ,
), am .SuffixExit ,
)
}
func RandId (strLen int ) string {
if strLen == 0 {
strLen = 16
}
strLen ++
strLen = strLen / 2
id := make ([]byte , strLen )
_ , err := rand .Read (id )
if err != nil {
return "error"
}
return hex .EncodeToString (id )
}
func WhenFunc (fn func ()) <-chan struct {} {
resChan := make (chan struct {})
go func () {
fn ()
close (resChan )
}()
return resChan
}
func WhenFuncOk (fn func () bool ) <-chan bool {
resChan := make (chan bool )
go func () {
if fn () {
resChan <- true
}
close (resChan )
}()
return resChan
}
The pages are generated with Golds v0.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 .