Files
mesh-sdk/go/handacts/handacts.go
T

159 lines
6.3 KiB
Go

// Package handacts is the hand-act log's contract (novox/hq ADR 0565 clauses 10 and 11, issue 1953): the layout of an act,
// moved unchanged from the controller's (mesh-controller internal/link HandAct) so one reader reads both logs alike; the
// names of the seat mesh-hand-acts and of what its holder keeps; and the recorder every module records through, which
// asks the seat and, while no holder answers, the controller's verb until the controller hands its lease away, saying
// which log took the act.
package handacts
import (
"encoding/json"
"errors"
"fmt"
"strings"
"time"
)
// The seat, its holder and what it keeps (ADR 0565 clause 10).
const (
Seat = "mesh-hand-acts"
Module = "mesh-hand-acts"
// ActsLog is the log of every act, kept 400 days; WarrantsState one key `<asker>.<ask>` per warrant, made once.
ActsLog = "acts"
WarrantsState = "warrants"
// EventRecorded is said on the seat for every act, with the act as its body.
EventRecorded = "act-recorded"
// ControllerActs is the controller's log as a reader names it, read until its lease ends.
ControllerActs = "mesh-controller.hand-acts"
)
// KeptFor is how long an act is kept.
const KeptFor = 400 * 24 * time.Hour
// The verbs an act is recorded under, as the controller's log names them.
const (
VerbRecord = "hand-act record"
VerbDrill = "hand-act drill"
VerbWarrant = "warrant"
// CauseDrill is a drill's cause, never counted as a repair (S15); CauseOperatorAnswer an act the operator chose.
CauseDrill = "drill"
CauseOperatorAnswer = "operator-answer"
// KindRecordedBuilds is a push that only moved recorded builds: no repair (ADR 0242).
KindRecordedBuilds = "recorded-builds"
)
// Act is one entry of the log, the controller's HandAct unchanged.
type Act struct {
ID string `json:"id"`
At time.Time `json:"at"`
// By is who: the caller the runtime proved.
By string `json:"by"`
// Verb and Args are the act as given.
Verb string `json:"verb"`
Args []string `json:"arguments,omitempty"`
Why string `json:"why"`
// Cause is the condition kind, or a word the person gives; the verb's own name when neither.
Cause string `json:"cause"`
Condition string `json:"condition,omitempty"`
// Kind is what the act was read to be from what it did, never a word a person gives.
Kind string `json:"kind,omitempty"`
Carried []string `json:"carried,omitempty"`
// Via, Ask, Proofs and RequestedBy are an act the operator chose on a warrant.
Via string `json:"via,omitempty"`
Ask string `json:"ask,omitempty"`
Proofs []string `json:"proofs,omitempty"`
RequestedBy string `json:"requested-by,omitempty"`
// Outcome is what came of an act recorded after it was done.
Outcome string `json:"outcome,omitempty"`
// Log is which log holds it, set when it is read (LogSeat or LogController); never written.
Log string `json:"log,omitempty"`
}
// The logs an act may be held in.
const (
LogSeat = Seat
LogController = "mesh-controller"
)
// Repair says the act counts toward a cause repeated (S15): not a drill, not an operator's answer, not recorded builds.
func (a Act) Repair() bool {
return a.Cause != CauseDrill && a.Verb != VerbDrill && a.Cause != CauseOperatorAnswer && a.Kind != KindRecordedBuilds
}
// Request is one act a recorder records: what, why, its cause, the condition it addresses, and its verb in the log (a
// repair when empty).
type Request struct {
What string `json:"what"`
Why string `json:"why"`
Cause string `json:"cause,omitempty"`
Condition string `json:"condition,omitempty"`
Verb string `json:"verb,omitempty"`
}
// Taken is which log took an act, and what its holder answered.
type Taken struct {
Log string `json:"log"`
Answer json.RawMessage `json:"answer,omitempty"`
}
// Recorder records acts for a module (ADR 0565 clause 11): through the seat mesh-hand-acts, and while the seat has no
// holder that answers, through the controller's verb until the controller hands its lease away (the release's order
// gives no seat's holder before its callers; 00-plan.md, "Missing" 1).
type Recorder struct {
// Ask calls a seat verb by its address (`seat:<seat>.<verb>`): stdio.Ask through the runtime.
Ask func(key string, body any) (json.RawMessage, error)
// LeaseAway says the controller's lease is handed away: then nothing falls back to it. Nil is never handed away.
LeaseAway func() bool
}
// NoResponder says err is the bus finding nobody to answer: the seat has no holder now.
func NoResponder(err error) bool {
return err != nil && strings.Contains(err.Error(), "no responders")
}
// Record records one act.
func (r Recorder) Record(q Request) (Taken, error) {
if strings.TrimSpace(q.What) == "" || strings.TrimSpace(q.Why) == "" {
return Taken{}, errors.New("a hand act says what was done and why: nothing was recorded")
}
body := map[string]any{"what": q.What, "why": q.Why}
for k, v := range map[string]string{"cause": q.Cause, "condition": q.Condition, "verb": q.Verb} {
if v != "" {
body[k] = v
}
}
controller := map[string]any{"what": q.What, "why": q.Why, "cause": q.Cause}
if q.Condition != "" {
controller["condition"] = q.Condition
}
if q.Cause == "" {
controller["cause"] = q.Verb
}
return r.either("hand-act", body, "hand-act", controller)
}
// RecordWarrant records what the asking module did on the operator's warrant for its ask.
func (r Recorder) RecordWarrant(asker, ask string) (Taken, error) {
body := map[string]any{"asker": asker, "ask": ask}
return r.either("warranted", body, "warranted", body)
}
func (r Recorder) either(verb string, body any, controllerVerb string, controllerBody any) (Taken, error) {
answer, err := r.Ask("seat:"+Seat+"."+verb, body)
if err == nil {
return Taken{Log: LogSeat, Answer: answer}, nil
}
if !NoResponder(err) {
return Taken{}, fmt.Errorf("%s.%s refused: %w", Seat, verb, err)
}
if r.LeaseAway != nil && r.LeaseAway() {
return Taken{}, fmt.Errorf("%s has no holder that answers, and the controller handed its lease away: nothing "+
"was recorded (%w)", Seat, err)
}
answer, cerr := r.Ask("seat:mesh-controller."+controllerVerb, controllerBody)
if cerr != nil {
return Taken{}, fmt.Errorf("%s has no holder that answers (%v), and the controller's %s refused too: %w", Seat, err,
controllerVerb, cerr)
}
return Taken{Log: LogController, Answer: answer}, nil
}