logo   async
machine.dev

/docs/schema.md

This document contains a brief overview of asyncmachine-go schema file formats. All schemas are simply an executable Golang code.

schema-v0

  • type safety NO
  • reflection NO
  • bootstrapped NO
  • inheritance NO
  • generated NO

Schemas for asyncmachine-go are optional and for simple things, we don’t need a typed schema (nor type safety), and we can construct the machine like so:

mach := newMach("DryWaterWet", am.Schema{
    "Wet": {
        Require: am.S{"Water"},
    },
    "Dry": {
        Remove: am.S{"Water"},
    },
    "Water": {
        Add:    am.S{"Wet"},
        Remove: am.S{"Dry"},
    },
})

All the state-related calls happen via raw strings. It’s good for prototyping and nothing else.

Schema-v1

  • type safety YES
  • reflection NO
  • bootstrapped NO
  • inheritance NO
  • generated NO

The v1 is the simplest schema file format:

package states

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

// S is a type alias for a list of state names.
type S = am.S

// States map defines relations and properties of states.
var States = am.Schema{
    CreatingExpense: {Remove: GroupExpense},
    ExpenseCreated:  {Remove: GroupExpense},
    WaitingForApproval: {
        Auto:   true,
        Remove: GroupApproval,
    },
    ApprovalGranted: {Remove: GroupApproval},
    PaymentInProgress: {
        Auto:   true,
        Remove: GroupPayment,
    },
    PaymentCompleted: {Remove: GroupPayment},
}

// Groups of mutually exclusive states.

var (
    GroupExpense  = S{CreatingExpense, ExpenseCreated}
    GroupApproval = S{WaitingForApproval, ApprovalGranted}
    GroupPayment  = S{PaymentInProgress, PaymentCompleted}
)

// #region boilerplate defs

// Names of all the states (pkg enum).

const (
    CreatingExpense    = "CreatingExpense"
    ExpenseCreated     = "ExpenseCreated"
    WaitingForApproval = "WaitingForApproval"
    ApprovalGranted    = "ApprovalGranted"
    PaymentInProgress  = "PaymentInProgress"
    PaymentCompleted   = "PaymentCompleted"
)

// Names is an ordered list of all the state names.
var Names = S{CreatingExpense, ExpenseCreated, WaitingForApproval, ApprovalGranted, PaymentInProgress, PaymentCompleted}

// #endregion

Usage:

import ss "github.com/pancsta/asyncmachine-go/examples/temporal_expense/states"

// ...

h := &MachineHandlers{}
mach, err := am.NewCommon(ctx, "expense", ss.Schema, ss.Names, h, nil, nil)

Schema-v2

  • type safety YES
  • reflection YES
  • bootstrapped YES
  • inheritance YES
  • generated NO

The v2 is the actual schema file format schema file format and should be used everywhere. The problem is the use of reflection that causes issues with TinyGo.

import (
    am "github.com/pancsta/asyncmachine-go/pkg/machine"
    "github.com/pancsta/asyncmachine-go/pkg/states"
    . "github.com/pancsta/asyncmachine-go/pkg/states/global"
)

// ...

// ServerStatesDef contains all the states of the Client state machine.
type ServerStatesDef struct {
    *am.StatesBase

    // basics

    // Ready - Client is fully connected to the server.
    Ready string

    // rpc

    // Starting listening
    RpcStarting string
    // setting up RPC accepting
    RpcAccepting string
    // RPC is accepting or has accepted connections
    RpcReady string

    // RPC client connected (technically)
    ClientConnected string
    // RPC client fully usable
    HandshakeDone string

    // How many times the client requested a full sync.
    MetricSync string
    // TCP tunneled over websocket
    WebSocketTunnel string

    // inherit from SharedStatesDef
    *SharedStatesDef
}

// ServerGroupsDef contains all the state groups of the Client state machine.
type ServerGroupsDef struct {
    *SharedGroupsDef

    // Rpc is a group for RPC ready states.
    Rpc S
}

// ServerSchema represents all relations and properties of ClientStates.
var ServerSchema = SchemaMerge(
    // inherit from SharedStruct
    SharedSchema,
    am.Schema{

        ssS.ErrNetwork: {
            Require: S{am.StateException},
            Remove:  S{ssS.ClientConnected},
        },

        // inject Server states into HandshakeDone
        ssS.HandshakeDone: StateAdd(
            SharedSchema[ssS.HandshakeDone],
            am.State{
                Require: S{ssS.ClientConnected},
                // TODO why?
                Remove: S{Exception},
            }),

        // Server

        ssS.Start: {Add: S{ssS.RpcStarting}},
        ssS.Ready: {
            Auto:    true,
            Require: S{ssS.HandshakeDone, ssS.RpcReady},
        },

        ssS.RpcStarting: {
            Require: S{ssS.Start},
            Remove:  sgS.Rpc,
        },
        ssS.RpcAccepting: {
            Require: S{ssS.Start},
            Remove:  sgS.Rpc,
        },
        ssS.RpcReady: {
            Require: S{ssS.Start},
            Remove:  sgS.Rpc,
        },
        ssS.ClientConnected: {
            Require: S{ssS.RpcReady},
        },

        ssS.MetricSync:      {Multi: true},
        ssS.WebSocketTunnel: {},
    })

// EXPORTS AND GROUPS

var (
    ssS = am.NewStates(ServerStatesDef{})
    sgS = am.NewStateGroups(ServerGroupsDef{
        Rpc: S{ssS.RpcStarting, ssS.RpcAccepting, ssS.RpcReady},
    }, SharedGroups)

    // ServerStates contains all the states for the Client machine.
    ServerStates = ssS
    // ServerGroups contains all the state groups for the Client machine.
    ServerGroups = sgS
)

See /tools/cmd/am-gen for schema bootstrapping commands.