Browse Source

Error handling design

Frederic G. MARAND 1 month ago
parent
commit
b10530c4af
21 changed files with 849 additions and 109 deletions
  1. 1 1
      LICENSE
  2. 75 0
      cmd/iter.go
  3. 82 0
      errors.go
  4. 72 0
      examples/alternate.go
  5. 51 0
      examples/cancel_async_transition.go
  6. 61 0
      examples/data.go
  7. 35 0
      examples/simple.go
  8. 10 0
      examples/states.dot
  9. 59 0
      examples/states.svg
  10. 53 0
      examples/struct.go
  11. 72 0
      examples/transition_callbacks.go
  12. 40 0
      feed.go
  13. 140 0
      fsm.go
  14. 0 100
      fsm2.go
  15. 6 1
      go.mod
  16. 4 0
      go.sum
  17. 4 6
      legacy/machine.go
  18. 1 1
      legacy/result.go
  19. 12 0
      transitions.go
  20. 51 0
      verifications.go
  21. 20 0
      verifications_test.go

+ 1 - 1
LICENSE

@@ -1,5 +1,5 @@
 MIT License
-Copyright (c) <year> <copyright holders>
+Copyright (c) 2005 Frederic G. MARAND <fgm@osinet.fr>
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 

+ 75 - 0
cmd/iter.go

@@ -0,0 +1,75 @@
+package main
+
+import (
+	"fmt"
+	"iter"
+)
+
+var max = 5
+
+func s0(yield func() bool) {
+	for i := 0; i < max; i++ {
+		res := yield()
+		fmt.Printf("s0 yield(%d) %t\n", i, res)
+	}
+}
+
+func s1(yield func(y int) bool) {
+	var res = true
+	// Stop on our terms, OR if a break was used in the for loop.
+	for i := 0; i < max && res; i++ {
+		res = yield(i)
+		fmt.Printf("s1 yield(%d) %t\n", i, res)
+	}
+}
+
+func s2(yield func(k int, v int) bool) {
+	for x, sq := 0, 0; sq < max; x++ {
+		sq = x * x
+		res := yield(x, sq)
+		fmt.Printf("yield(%d, %d) %t\n", x, sq, res)
+	}
+}
+
+func main() {
+	//for range s0 {
+	//	fmt.Print("In range s0: ")
+	//}
+	//fmt.Println()
+	//
+	//for k := range s1 {
+	//	fmt.Print("In range s1 ")
+	//	if k > max/2 {
+	//		break
+	//	}
+	//}
+	//fmt.Println()
+	//
+	//for k := range s2 {
+	//	fmt.Print("In range s2 ")
+	//	if k > max/2 {
+	//		break
+	//	}
+	//}
+	//fmt.Println()
+	//
+	//for k, v := range s2 {
+	//	fmt.Printf("In range s2 %d/%d ", k, v)
+	//	if k > max/2 {
+	//		break
+	//	}
+	//}
+
+	next, stop := iter.Pull(s1)
+	nw := func() (int, bool) {
+		k, ok := next()
+		fmt.Printf("next(%d) %t\n", k, ok)
+		return k, ok
+	}
+	for k, ok := nw(); ok; k, ok = next() {
+		fmt.Printf("k: %d, ok: %t\n", k, ok)
+		if k > max/2 {
+			stop()
+		}
+	}
+}

+ 82 - 0
errors.go

@@ -0,0 +1,82 @@
+package fsm
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"log"
+)
+
+type (
+	// ErrorString is a specific error type used to recognize FSM errors.
+	ErrorString string
+
+	// OnError is the type of FSM optional error handlers.
+	OnError[SK, EK comparable] func(SK, EK, error) error
+)
+
+const (
+	ErrUnavailableEvent ErrorString = "unavailable event"
+	ErrGuardFailure     ErrorString = "guard failure"
+	ErrActionFailure    ErrorString = "action failure"
+	ErrEnterFailure     ErrorString = "enter failure"
+	ErrLeaveFailure     ErrorString = "leave failure"
+)
+
+func (e ErrorString) Error() string {
+	return string(e)
+}
+
+func MakeFailureError[SK, EK comparable](sk SK, ek EK, err error) error {
+	var es ErrorString
+	if ok := errors.As(err, &es); !ok {
+		return fmt.Errorf("generic error at state %s / event %s: %w", sk, ek, err)
+	}
+
+	var format string
+	switch es {
+	case ErrUnavailableEvent:
+		format = "unavailable event %s received on state %s: %w"
+	case ErrGuardFailure:
+		format = "guard failed for event %s received on state %s: %w"
+	case ErrActionFailure:
+		format = "action failed after event %s received on state %s: %w"
+	case ErrEnterFailure:
+		format = "enter failed entering state %s after event %s: %w"
+	case ErrLeaveFailure:
+		format = "leave failed leaving state %s on event %s: %w"
+	default:
+		format = "custom error at state %s / event %s: %w"
+	}
+	return fmt.Errorf(format, sk, ek, es)
+}
+
+// OnErrorIgnore silently ignores errors. Usually not recommended: mostly useful in tests.
+func OnErrorIgnore[SK, EK comparable](_ SK, _ EK, _ error) error { return nil }
+
+// OnErrorReturn returns the received error and, allowing the FSM to act on it.
+func OnErrorReturn[SK, EK comparable](sk SK, ek EK, err error) error {
+	return MakeFailureError(sk, ek, err)
+}
+
+// OnErrorPanic panics when receiving an error. Not recommended.
+func OnErrorPanic[SK, EK comparable](sk SK, ek EK, err error) error {
+	panic(MakeFailureError(sk, ek, err))
+}
+
+// MakeOnErrorWrite logs the error to the specified writer and returns success.
+func MakeOnErrorWrite[SK, EK comparable](w io.Writer) OnError[SK, EK] {
+	return func(sk SK, ek EK, err error) error {
+		fmt.Fprintln(w, MakeFailureError(sk, ek, err))
+		return nil
+	}
+}
+
+// _ is a compile-time verification of the type conformity for OnError handlers
+func _() {
+	var fsm FSM[string, string, string]
+	fsm.SetOnUnavailable(OnErrorIgnore[string, string])
+	fsm.SetOnUnavailable(OnErrorReturn[string, string])
+	fsm.SetOnUnavailable(OnErrorPanic[string, string])
+	fsm.SetOnUnavailable(MakeOnErrorWrite[string, string](log.Default().Writer()))
+}

+ 72 - 0
examples/alternate.go

@@ -0,0 +1,72 @@
+package main
+
+import (
+	"context"
+	"errors"
+	"fmt"
+
+	ll "github.com/looplab/fsm"
+)
+
+func isNoTransitionError(err error) bool {
+	var noTransitionError ll.NoTransitionError
+	return errors.As(err, &noTransitionError)
+}
+
+func main() {
+	fsm := ll.NewFSM(
+		"idle",
+		ll.Events{
+			{Name: "scan", Src: []string{"idle"}, Dst: "scanning"},
+			{Name: "working", Src: []string{"scanning"}, Dst: "scanning"},
+			{Name: "situation", Src: []string{"scanning"}, Dst: "scanning"},
+			{Name: "situation", Src: []string{"idle"}, Dst: "idle"},
+			{Name: "finish", Src: []string{"scanning"}, Dst: "idle"},
+		},
+		ll.Callbacks{
+			"scan": func(_ context.Context, e *ll.Event) {
+				fmt.Println("after_scan: " + e.FSM.Current())
+			},
+			"working": func(_ context.Context, e *ll.Event) {
+				fmt.Println("working: " + e.FSM.Current())
+			},
+			"situation": func(_ context.Context, e *ll.Event) {
+				fmt.Println("situation: " + e.FSM.Current())
+			},
+			"finish": func(_ context.Context, e *ll.Event) {
+				fmt.Println("finish: " + e.FSM.Current())
+			},
+		},
+	)
+
+	fmt.Println(fsm.Current())
+
+	err := fsm.Event(context.Background(), "scan")
+	if err != nil {
+		fmt.Println(err)
+	}
+
+	fmt.Println("1:" + fsm.Current())
+
+	err = fsm.Event(context.Background(), "working")
+	if err != nil && !isNoTransitionError(err) {
+		fmt.Println(err)
+	}
+
+	fmt.Println("2:" + fsm.Current())
+
+	err = fsm.Event(context.Background(), "situation")
+	if err != nil && !isNoTransitionError(err) {
+		fmt.Println(err)
+	}
+
+	fmt.Println("3:" + fsm.Current())
+
+	err = fsm.Event(context.Background(), "finish")
+	if err != nil && !isNoTransitionError(err) {
+		fmt.Println(err)
+	}
+
+	fmt.Println("4:" + fsm.Current())
+
+}

+ 51 - 0
examples/cancel_async_transition.go

@@ -0,0 +1,51 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/looplab/fsm"
+)
+
+func main() {
+	f := fsm.NewFSM(
+		"start",
+		fsm.Events{
+			{Name: "run", Src: []string{"start"}, Dst: "end"},
+		},
+		fsm.Callbacks{
+			"leave_start": func(_ context.Context, e *fsm.Event) {
+				e.Async()
+			},
+		},
+	)
+
+	err := f.Event(context.Background(), "run")
+	asyncError, ok := err.(fsm.AsyncError)
+	if !ok {
+		panic(fmt.Sprintf("expected error to be 'AsyncError', got %v", err))
+	}
+	var asyncStateTransitionWasCanceled bool
+	go func() {
+		<-asyncError.Ctx.Done()
+		asyncStateTransitionWasCanceled = true
+		if asyncError.Ctx.Err() != context.Canceled {
+			panic(fmt.Sprintf("Expected error to be '%v' but was '%v'", context.Canceled, asyncError.Ctx.Err()))
+		}
+	}()
+	asyncError.CancelTransition()
+	time.Sleep(20 * time.Millisecond)
+
+	if err = f.Transition(); err != nil {
+		panic(fmt.Sprintf("Error encountered when transitioning: %v", err))
+	}
+	if !asyncStateTransitionWasCanceled {
+		panic("expected async state transition cancelation to have propagated")
+	}
+	if f.Current() != "start" {
+		panic("expected state to be 'start'")
+	}
+
+	fmt.Println("Successfully ran state machine.")
+}

+ 61 - 0
examples/data.go

@@ -0,0 +1,61 @@
+package main
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/looplab/fsm"
+)
+
+func main() {
+	fsm := fsm.NewFSM(
+		"idle",
+		fsm.Events{
+			{Name: "produce", Src: []string{"idle"}, Dst: "idle"},
+			{Name: "consume", Src: []string{"idle"}, Dst: "idle"},
+			{Name: "remove", Src: []string{"idle"}, Dst: "idle"},
+		},
+		fsm.Callbacks{
+			"produce": func(_ context.Context, e *fsm.Event) {
+				e.FSM.SetMetadata("message", "hii")
+				fmt.Println("produced data")
+			},
+			"consume": func(_ context.Context, e *fsm.Event) {
+				message, ok := e.FSM.Metadata("message")
+				if ok {
+					fmt.Println("message = " + message.(string))
+				}
+			},
+			"remove": func(_ context.Context, e *fsm.Event) {
+				e.FSM.DeleteMetadata("message")
+				if _, ok := e.FSM.Metadata("message"); !ok {
+					fmt.Println("message removed")
+				}
+			},
+		},
+	)
+
+	fmt.Println(fsm.Current())
+
+	err := fsm.Event(context.Background(), "produce")
+	if err != nil {
+		fmt.Println(err)
+	}
+
+	fmt.Println(fsm.Current())
+
+	err = fsm.Event(context.Background(), "consume")
+	if err != nil {
+		fmt.Println(err)
+	}
+
+	fmt.Println(fsm.Current())
+
+	err = fsm.Event(context.Background(), "remove")
+	if err != nil {
+		fmt.Println(err)
+	}
+
+	fmt.Println(fsm.Current())
+
+}

+ 35 - 0
examples/simple.go

@@ -0,0 +1,35 @@
+package main
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/looplab/fsm"
+)
+
+func main() {
+	fsm := fsm.NewFSM(
+		"closed",
+		fsm.Events{
+			{Name: "open", Src: []string{"closed"}, Dst: "open"},
+			{Name: "close", Src: []string{"open"}, Dst: "closed"},
+		},
+		fsm.Callbacks{},
+	)
+
+	fmt.Println(fsm.Current())
+
+	err := fsm.Event(context.Background(), "open")
+	if err != nil {
+		fmt.Println(err)
+	}
+
+	fmt.Println(fsm.Current())
+
+	err = fsm.Event(context.Background(), "close")
+	if err != nil {
+		fmt.Println(err)
+	}
+
+	fmt.Println(fsm.Current())
+}

+ 10 - 0
examples/states.dot

@@ -0,0 +1,10 @@
+digraph fsm {
+    "end" -> "finished" [ label = "finish" ];
+    "end" -> "start" [ label = "reset" ];
+    "finished" -> "start" [ label = "reset" ];
+    "start" -> "end" [ label = "run" ];
+
+    "end";
+    "finished";
+    "start" [color = "red"];
+}

+ 59 - 0
examples/states.svg

@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 12.2.1 (20241206.2353)
+ -->
+<!-- Title: fsm Pages: 1 -->
+<svg width="176pt" height="221pt"
+ viewBox="0.00 0.00 176.03 221.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 217)">
+<title>fsm</title>
+<polygon fill="white" stroke="none" points="-4,4 -4,-217 172.03,-217 172.03,4 -4,4"/>
+<!-- end -->
+<g id="node1" class="node">
+<title>end</title>
+<ellipse fill="none" stroke="black" cx="117.6" cy="-18" rx="27" ry="18"/>
+<text text-anchor="middle" x="117.6" y="-12.95" font-family="Times,serif" font-size="14.00">end</text>
+</g>
+<!-- finished -->
+<g id="node2" class="node">
+<title>finished</title>
+<ellipse fill="none" stroke="black" cx="40.6" cy="-106.5" rx="40.6" ry="18"/>
+<text text-anchor="middle" x="40.6" y="-101.45" font-family="Times,serif" font-size="14.00">finished</text>
+</g>
+<!-- end&#45;&gt;finished -->
+<g id="edge1" class="edge">
+<title>end&#45;&gt;finished</title>
+<path fill="none" stroke="black" d="M98.96,-31.33C90.42,-37.52 80.51,-45.5 72.85,-54 66.13,-61.47 59.94,-70.51 54.82,-78.92"/>
+<polygon fill="black" stroke="black" points="51.89,-77 49.91,-87.41 57.95,-80.51 51.89,-77"/>
+<text text-anchor="middle" x="88.23" y="-57.2" font-family="Times,serif" font-size="14.00">finish</text>
+</g>
+<!-- start -->
+<g id="node3" class="node">
+<title>start</title>
+<ellipse fill="none" stroke="red" cx="117.6" cy="-195" rx="27" ry="18"/>
+<text text-anchor="middle" x="117.6" y="-189.95" font-family="Times,serif" font-size="14.00">start</text>
+</g>
+<!-- end&#45;&gt;start -->
+<g id="edge2" class="edge">
+<title>end&#45;&gt;start</title>
+<path fill="none" stroke="black" d="M117.6,-36.42C117.6,-66.56 117.6,-128.7 117.6,-165.3"/>
+<polygon fill="black" stroke="black" points="114.1,-165.14 117.6,-175.14 121.1,-165.14 114.1,-165.14"/>
+<text text-anchor="middle" x="130.35" y="-101.45" font-family="Times,serif" font-size="14.00">reset</text>
+</g>
+<!-- finished&#45;&gt;start -->
+<g id="edge3" class="edge">
+<title>finished&#45;&gt;start</title>
+<path fill="none" stroke="black" d="M51.25,-123.97C58.33,-134.43 68.14,-148.03 78.1,-159 82.42,-163.75 87.32,-168.51 92.18,-172.94"/>
+<polygon fill="black" stroke="black" points="89.76,-175.47 99.58,-179.45 94.38,-170.21 89.76,-175.47"/>
+<text text-anchor="middle" x="90.85" y="-145.7" font-family="Times,serif" font-size="14.00">reset</text>
+</g>
+<!-- start&#45;&gt;end -->
+<g id="edge4" class="edge">
+<title>start&#45;&gt;end</title>
+<path fill="none" stroke="black" d="M126.87,-177.78C134.13,-164.07 143.64,-143.68 147.6,-124.5 150.84,-108.83 150.84,-104.17 147.6,-88.5 144.54,-73.67 138.15,-58.11 132.06,-45.48"/>
+<polygon fill="black" stroke="black" points="135.19,-43.92 127.55,-36.57 128.95,-47.08 135.19,-43.92"/>
+<text text-anchor="middle" x="159.03" y="-101.45" font-family="Times,serif" font-size="14.00">run</text>
+</g>
+</g>
+</svg>

+ 53 - 0
examples/struct.go

@@ -0,0 +1,53 @@
+//go:build ignore
+// +build ignore
+
+package main
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/looplab/fsm"
+)
+
+type Door struct {
+	To  string
+	FSM *fsm.FSM
+}
+
+func NewDoor(to string) *Door {
+	d := &Door{
+		To: to,
+	}
+
+	d.FSM = fsm.NewFSM(
+		"closed",
+		fsm.Events{
+			{Name: "open", Src: []string{"closed"}, Dst: "open"},
+			{Name: "close", Src: []string{"open"}, Dst: "closed"},
+		},
+		fsm.Callbacks{
+			"enter_state": func(_ context.Context, e *fsm.Event) { d.enterState(e) },
+		},
+	)
+
+	return d
+}
+
+func (d *Door) enterState(e *fsm.Event) {
+	fmt.Printf("The door to %s is %s\n", d.To, e.Dst)
+}
+
+func main() {
+	door := NewDoor("heaven")
+
+	err := door.FSM.Event(context.Background(), "open")
+	if err != nil {
+		fmt.Println(err)
+	}
+
+	err = door.FSM.Event(context.Background(), "close")
+	if err != nil {
+		fmt.Println(err)
+	}
+}

+ 72 - 0
examples/transition_callbacks.go

@@ -0,0 +1,72 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"log"
+
+	"github.com/looplab/fsm"
+)
+
+func main() {
+	var afterFinishCalled bool
+	cbs := make(fsm.Callbacks)
+	for _, name := range []string{
+		// Events: before, after
+		"run",
+		"finish",
+		"reset",
+		// States: enter, leave
+		"start",
+		"end",
+		"finished",
+	} {
+		for _, op := range []string{"before_", "after_", "leave_", "enter_"} {
+			cbs[op+name] = func(ctx context.Context, event *fsm.Event) {
+				log.Printf("%s", op+name)
+			}
+		}
+	}
+	cbs["enter_end"] = func(ctx context.Context, e *fsm.Event) {
+		log.Println("enter end")
+		if err := e.FSM.Event(ctx, "finish"); err != nil {
+			fmt.Println(err)
+		}
+	}
+	cbs["after_finish"] = func(ctx context.Context, e *fsm.Event) {
+		log.Println("after finish")
+		afterFinishCalled = true
+		if e.Src != "end" {
+			panic(fmt.Sprintf("source should have been 'end' but was '%s'", e.Src))
+		}
+		if err := e.FSM.Event(ctx, "reset"); err != nil {
+			fmt.Println(err)
+		}
+	}
+	machine := fsm.NewFSM(
+		"start",
+		fsm.Events{
+			{Name: "run", Src: []string{"start"}, Dst: "end"},
+			{Name: "finish", Src: []string{"end"}, Dst: "finished"},
+			{Name: "reset", Src: []string{"end", "finished"}, Dst: "start"},
+		},
+		cbs,
+	)
+
+	if err := machine.Event(context.Background(), "run"); err != nil {
+		panic(fmt.Sprintf("Error encountered when triggering the run event: %v", err))
+	}
+
+	if !afterFinishCalled {
+		//panic(fmt.Sprintf("After finish callback should have run, current state: '%s'", machine.Current()))
+	}
+
+	currentState := machine.Current()
+	if currentState != "start" {
+		panic(fmt.Sprintf("expected state to be 'start', was '%s'", currentState))
+	}
+
+	fmt.Println("Successfully ran state machine.")
+	//fmt.Println(fsm.VisualizeForMermaidWithGraphType(machine, fsm.StateDiagram))
+	fmt.Println(fsm.VisualizeWithType(machine, fsm.GRAPHVIZ))
+}

+ 40 - 0
feed.go

@@ -0,0 +1,40 @@
+package fsm
+
+import (
+	"time"
+)
+
+const (
+	// TickName is the name of Tick events, which Transition instances can use to enable
+	// automatic progress on FSM instances in pull mode.
+	TickName = "tick"
+)
+
+type tickEvent[T any] struct {
+	data T
+}
+
+func (t tickEvent[T]) Name() string {
+	return TickName
+}
+
+func (t tickEvent[T]) Data() T {
+	return t.data
+}
+
+// MakeChannelFeed builds a Feed enabling pull mode on an FSM.
+func MakeChannelFeed[T any](ch chan T, tick time.Duration) Feed[T] {
+	ticker := time.NewTicker(tick)
+	return func(fsm FSM[T]) Event[T] {
+		var e Event[T]
+		select {
+		case <-fsm.Context().Done():
+			ticker.Stop()
+			return e
+		case <-ticker.C:
+			return tickEvent[T]{}
+		case e = <-ch:
+			return e
+		}
+	}
+}

+ 140 - 0
fsm.go

@@ -0,0 +1,140 @@
+// Package fsm implements a finite state machine with specific features:
+//
+// # Operation
+//
+//   - All data types are parametric and checked: state key, event key, and event data
+//   - Low-level (push) mode: clients call its Handle method, feeding events to these calls
+//   - High-level (pull) mode: machine receives events from a Feed
+//     This enables automatic progress on clock ticks, generating tick events which can be Transition triggers.
+//   - Feed for high-level mode is injectable, defaulting to a channel.
+//
+// # Hooks
+//
+//   - Events support guards and action hooks.
+//   - States support enter and leave hooks
+//   - Hooks have a unified signature
+//
+// # Event payloads
+//
+//   - Events can carry a typed payload, available to all hooks.
+//   - All machine types are parameterized on it, enabling strong typing.
+//
+// # Contexts
+//
+//   - Machine carries a context enabling global cancellation
+//   - Handlers all take a per-event context enabling individual cancellation
+//   - Handler contexts should be either nil, which uses the machine context, or based on that context.
+package fsm
+
+import (
+	"context"
+	"time"
+)
+
+type Hook[K comparable, EV any] func(ctx context.Context, event Event[K, EV]) error
+
+type State[SK, EK comparable, EV any] interface {
+	Name() SK
+	SetAfterEnter([]Hook[EK, EV])
+	AfterEnter() []Hook[EK, EV]
+	SetBeforeLeave([]Hook[EK, EV])
+	BeforeLeave() []Hook[EK, EV]
+}
+
+type Event[EK comparable, EV any] interface {
+	Name() EK
+	Data() EV
+}
+
+type RetryableError interface {
+	error
+	RetryableError()
+}
+
+type Transition[SK, EK comparable, EV any] interface {
+	SetGuards([]Hook[EK, EV])
+	Guards() []Hook[EK, EV]
+	SetActions([]Hook[EK, EV])
+	Actions() []Hook[EK, EV]
+	Next() SK
+}
+
+// Matrix describes the set of Transition defining an FSM.
+type Matrix[SK, EK comparable, EV any] map[SK]map[EK]Transition[SK, EK, EV]
+
+// BackoffFunc returning <0 means the retry limit has been reached.
+type BackoffFunc func() time.Duration
+
+type FSM[SK, EK comparable, EV any] interface {
+	Context() context.Context
+	Name() string
+
+	SetBackoff(fn BackoffFunc)
+
+	// SetFeed allows the FSM to support pull mode.
+	SetFeed(feed Feed[SK, EK, EV])
+
+	// SetMatrix defines the Transition matrix on an FSM.
+	SetMatrix(mx Matrix[SK, EK, EV])
+
+	// SetOnUnavailable defines optional behaviour when receiving an event not accepted by the current state.
+	// Default is OnErrorIgnore.
+	SetOnUnavailable(OnError[SK, EK])
+	// SetOnGuardFailure defines optional behaviour when a guard rejects a transition attempt.
+	// Default is OnErrorReturn.
+	SetOnGuardFailure(OnError[SK, EK])
+	// SetOnActionFailure defines optional behaviour when failing an event action.
+	// Default is OnErrorReturn.
+	SetOnActionFailure(OnError[SK, EK])
+	// SetOnEnterFailure defines optional behaviour when a state entry fails.
+	// Default is OnErrorReturn.
+	SetOnEnterFailure(OnError[SK, EK])
+	// SetOnLeaveFailure defines optional behaviour when a state leave fails.
+	// Default is OnErrorReturn.
+	SetOnLeaveFailure(OnError[SK, EK])
+
+	StartState() State[SK, EK, EV]
+	EndState() State[SK, EK, EV]
+	ErrorState() State[SK, EK, EV]
+}
+
+type PushMachine[SK, EK comparable, EV any] interface {
+	FSM[SK, EK, EV]
+	Handle(context.Context, Event[EK, EV]) error // If ctx == nil, handling uses the machine context.
+}
+
+type PullMachine[SK, EK comparable, EV any] interface {
+	FSM[SK, EK, EV]
+	Start(ctx context.Context, events chan<- Event[EK, EV]) State[SK, EK, EV]
+}
+
+var (
+	BackoffInitialDelay    = time.Millisecond
+	BackoffMaxAttempts     = 10
+	DefaultBackoffAttempts = 0
+
+	// NoBackoff retries without delay, but only up to the maximum number of retries.
+	NoBackoff BackoffFunc = func() time.Duration {
+		DefaultBackoffAttempts++
+		if DefaultBackoffAttempts > BackoffMaxAttempts {
+			return -1
+		}
+		return YoloBackoff()
+	}
+
+	// ExponentialBackoff retries with an exponential delay and maximum number of retries.
+	ExponentialBackoff BackoffFunc = func() time.Duration {
+		d := NoBackoff()
+		if d < 0 {
+			return d
+		}
+		return (1 << DefaultBackoffAttempts) * BackoffInitialDelay
+	}
+
+	// YoloBackoff retries indefinitely without delay. Not a good idea outside specific test cases.
+	YoloBackoff BackoffFunc = func() time.Duration {
+		return 0
+	}
+)
+
+type Feed[SK, EK comparable, EV any] func(fsm FSM[SK, EK, EV]) Event[EK, EV]

+ 0 - 100
fsm2.go

@@ -1,100 +0,0 @@
-package fsm
-
-import (
-	"context"
-	"time"
-)
-
-type M2Hook[T any] func(ctx context.Context, event M2Event[T]) error
-
-type M2State[T any] interface {
-	Name() string
-	SetAfterEnter([]M2Hook[T])
-	AfterEnter() []M2Hook[T]
-	SetBeforeLeave([]M2Hook[T])
-	BeforeLeave() []M2Hook[T]
-}
-
-type M2Event[T any] interface {
-	Name() string
-	Data() T
-}
-
-type RetryableError interface {
-	error
-	RetryableError()
-}
-
-type M2Transition[T any] struct {
-	guards, actions []M2Hook[T]
-	next            State
-}
-
-func (mt *M2Transition[T]) SetGuards([]M2Hook[T])  {}
-func (mt *M2Transition[T]) Guards() []M2Hook[T]    { return mt.guards }
-func (mt *M2Transition[T]) SetActions([]M2Hook[T]) {}
-func (mt *M2Transition[T]) Actions() []M2Hook[T]   { return mt.actions }
-func (mt *M2Transition[T]) Next() State            { return mt.next }
-
-type M2Matrix[T any] map[M2State[T]]map[M2Event[T]]M2Transition[T]
-
-// M2BackoffFunc returning <0 means the retry limit has been reached.
-type M2BackoffFunc func() time.Duration
-
-type M2FSM[T any] interface {
-	Name() string
-
-	SetBackoff(fn M2BackoffFunc)
-
-	SetMatrix(mx M2Matrix[T])
-	Matrix() M2Matrix[T]
-
-	StartState() M2State[T]
-	EndState() M2State[T]
-	ErrorState() M2State[T]
-}
-
-type M2PushMachine[T any] interface {
-	M2FSM[T]
-	Handle(context.Context, M2Event[T]) error // If ctx == nil, handling uses the machine context.
-}
-
-type M2PullMachine[T any] interface {
-	M2FSM[T]
-	Start(ctx context.Context, events chan<- M2Event[T]) M2State[T]
-}
-
-type M2VerifiableMachine[T any] interface {
-	M2FSM[T]
-	IsReachable(s1, s2 M2State[T]) bool
-	IsStronglyConnected() bool // Minimal Complexity O(V+E) using DFS (Tarjan or Kosaraju)
-}
-
-var (
-	m2BackoffInitialDelay    = time.Millisecond
-	m2BackoffMaxAttempts     = 10
-	m2DefaultBackoffAttempts = 0
-
-	// M2YoloBackoff retries indefinitely without delay.
-	M2YoloBackoff M2BackoffFunc = func() time.Duration {
-		return 0
-	}
-
-	// M2NoBackoff retries immediately but only up to the maximum number of retries.
-	M2NoBackoff M2BackoffFunc = func() time.Duration {
-		m2DefaultBackoffAttempts++
-		if m2DefaultBackoffAttempts > m2BackoffMaxAttempts {
-			return -1
-		}
-		return M2YoloBackoff()
-	}
-
-	// M2ExponentialBackoff retries with an exponential delay and maximum number of retries.
-	M2ExponentialBackoff M2BackoffFunc = func() time.Duration {
-		d := M2NoBackoff()
-		if d < 0 {
-			return d
-		}
-		return (1 << m2DefaultBackoffAttempts) * m2BackoffInitialDelay
-	}
-)

+ 6 - 1
go.mod

@@ -1,5 +1,10 @@
 module code.osinet.fr/fgm/fsm
 
-go 1.20
+go 1.23
 
 require golang.org/x/exp v0.0.0-20230321023759-10a507213a29
+
+require (
+	github.com/cocoonspace/fsm v1.0.1 // indirect
+	github.com/looplab/fsm v1.0.2 // indirect
+)

+ 4 - 0
go.sum

@@ -1,2 +1,6 @@
+github.com/cocoonspace/fsm v1.0.1 h1:dPjDYJh1XL7r8jERbAVFrk7kLzTOaRgx9EBoS9fV6PA=
+github.com/cocoonspace/fsm v1.0.1/go.mod h1:2NILMnDanocMM88qdqAzyq9Q1DwCjXqAIGtbhdXFC0Q=
+github.com/looplab/fsm v1.0.2 h1:f0kdMzr4CRpXtaKKRUxwLYJ7PirTdwrtNumeLN+mDx8=
+github.com/looplab/fsm v1.0.2/go.mod h1:PmD3fFvQEIsjMEfvZdrCDZ6y8VwKTwWNjlpEr6IKPO4=
 golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
 golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=

+ 4 - 6
machine.go → legacy/machine.go

@@ -1,16 +1,14 @@
 /*
-Package fsm defines an embeddable finite state machine.
-
-In its initial version, it is a straight port of the 2012 PHP
-OSInet\Finite_State_Machine\Machine available in https://code.osinet.fr/fgm/php__phplib.git
+Package fsm defines an embeddable finite state machine, as a straight port of the 2012 PHP
+[OSInet\Finite_State_Machine\Machine](https://code.osinet.fr/fgm/php__phplib.git]
 
 See http://wiki.audean.com/fsm/fsm for details.
 
 Copyright  (c) 2023 Ouest Systèmes Informatiques
 
-License    GPL-3.0
+License GPL-3.0
 */
-package fsm
+package legacy
 
 import (
 	"errors"

+ 1 - 1
result.go → legacy/result.go

@@ -1,4 +1,4 @@
-package fsm
+package legacy
 
 // Result defines a possible outcome for a given FSM Transition.
 type Result struct {

+ 12 - 0
transitions.go

@@ -0,0 +1,12 @@
+package fsm
+
+type transition[SK, EK comparable, EV any] struct {
+	guards, actions []Hook[EK, EV]
+	next            SK
+}
+
+func (mt *transition[SK, EK, EV]) SetGuards([]Hook[EK, EV])  {}
+func (mt *transition[SK, EK, EV]) Guards() []Hook[EK, EV]    { return mt.guards }
+func (mt *transition[SK, EK, EV]) SetActions([]Hook[EK, EV]) {}
+func (mt *transition[SK, EK, EV]) Actions() []Hook[EK, EV]   { return mt.actions }
+func (mt *transition[SK, EK, EV]) Next() SK                  { return mt.next }

+ 51 - 0
verifications.go

@@ -0,0 +1,51 @@
+package fsm
+
+// IsComplete verifies that every pair or states has a defined transition.
+//
+// Complexity: O(V) if the Matrix is the default map implementation.
+func IsComplete[SK, EK comparable, EV any](Matrix[SK, EK, EV]) bool {
+	panic("not implemented")
+}
+
+// InCompleteWithoutSelfLoops verifies that every pair of different states has a defined transition,
+// but no same-state transition exists.
+func InCompleteWithoutSelfLoops[SK, EK comparable, EV any]() bool {
+	panic("not implemented")
+}
+
+// IsReachable verifies that s2 is reachable from s1.
+func IsReachable[SK, EK comparable, EV any](s1, s2 State[SK, EK, EV]) bool {
+	panic("not implemented")
+}
+
+// IsAccessible verifies that every state except initial is reachable from the initial state.
+func IsAccessible[SK, EK comparable, EV any]() bool {
+	panic("not implemented")
+}
+
+// IsCoAccessible verifies that final state is reachable from any state except final.
+func IsCoAccessible[SK, EK comparable, EV any]() bool {
+	panic("not implemented")
+}
+
+// IsTrimmed verifies that the matrix verifies both IsAccessible and IsCoAccessible.
+func IsTrimmed[SK, EK comparable, EV any]() bool {
+	panic("not implemented")
+}
+
+// IsStronglyConnected verifies that every state is reachable from every other.
+//
+// Minimal Complexity O(V+E) using DFS (Tarjan or Kosaraju)
+func IsStronglyConnected[SK, EK comparable, EV any]() bool {
+	panic("not implemented")
+}
+
+// IsFSMContextCancellable verifies that the context on a FSM is set to a context
+// that includes a cancelCtx.
+//
+// WARNING: this assumes that all context stacked above it implement the context.stringer interface
+// and respect the unofficial context package naming convention for implementations of that interface.
+// All types in stdlib context do, but customer context implementations might not.
+func IsFSMContextCancellable[SK, EK comparable, EV any](FSM[SK, EK, EV]) bool {
+	panic("not implemented")
+}

+ 20 - 0
verifications_test.go

@@ -0,0 +1,20 @@
+package fsm
+
+import (
+	"testing"
+)
+
+func TestIsFSMContextCancellable(t *testing.T) {
+	/* Example case:
+	ctx := context.TODO()
+	ctx, can := context.WithCancel(ctx)
+	fmt.Fprintln(io.Discard, can)
+	ctx, canD := context.WithDeadline(ctx, time.Now().Add(time.Minute))
+	fmt.Fprintln(io.Discard, canD)
+	ctx = context.WithoutCancel(ctx)
+	ctx, canT := context.WithTimeout(ctx, time.Minute)
+	fmt.Fprintln(io.Discard, canT)
+	sCtx, ok := ctx.(fmt.Stringer)
+	fmt.Println(sCtx.String(), ok)
+	*/
+}