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

197 lines
6.4 KiB
Go

// Package asker is a module's client for asking a person (novox/hq ADR 0259 §10): it publishes an ask under
// the module's own name, cancels it, reads its state from the router's record, and hands the module each
// warrant once — only one for an ask it made, for an option it offered, at that option's level, before it
// expired. The module then acts with its own grants.
//
// It keeps what it asked, and which warrants it acted on, in a state of the module's own (declared in its
// manifest, by default `asked`): the ask is written before it is published, and a warrant is marked acted on
// by compare-and-set before the module's handler runs, so a redelivery, or a second instance, acts once.
package asker
import (
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asks"
)
// Client asks as one module.
type Client struct {
// Module is the asking module's name: the one the bus grants its asks under.
Module string
// State is the module's own state the asks are kept in.
State Store
// Now is the clock; nil is time.Now.
Now func() time.Time
// Publish, Record and Subscribe reach the bus; nil is the stdio SDK's, through the module's runtime.
Publish func(subject string, body any, id string) error
Record func(key string) (*stdio.StateEntry, error)
Subscribe func(pattern string, handle func(stdio.Envelope) error) error
}
// Store is where the asks are kept: stdio.State(name) is one.
type Store interface {
Get(key string) (*stdio.StateEntry, error)
Create(key string, value any) (uint64, error)
Update(key string, value any, revision uint64) (uint64, error)
}
// New is a client for the module the runtime serves, keeping its asks in the state named.
func New(state string) *Client {
return &Client{Module: os.Getenv("MESH_MODULE"), State: stdio.State(state)}
}
func (c *Client) now() time.Time {
if c.Now != nil {
return c.Now()
}
return time.Now()
}
// Kept is one ask as the module keeps it.
type Kept struct {
Ask asks.Ask `json:"ask"`
Asked time.Time `json:"asked"`
Acted bool `json:"acted,omitempty"`
Ended asks.Outcome `json:"ended,omitempty"`
Answer *asks.Warrant `json:"answer,omitempty"`
}
func key(id string) string { return "ask." + id }
// Ask checks an ask, keeps it, and publishes it under the module's own name. An ask with an id already kept
// is refused: a new question is a new id.
func (c *Client) Ask(a asks.Ask) error {
if err := a.Check(c.now()); err != nil {
return err
}
if _, err := c.State.Create(key(a.ID), Kept{Ask: a, Asked: c.now()}); err != nil {
if errors.Is(err, stdio.ErrChanged) {
return fmt.Errorf("an ask %s was already made: a new question is a new id", a.ID)
}
return err
}
return c.publish(asks.AskSubject(c.Module), a, a.ID)
}
// Cancel takes an ask back: the router ends it and edits every copy.
func (c *Client) Cancel(id string) error {
return c.publish(asks.CancelSubject(c.Module), map[string]string{"id": id}, "cancel."+id)
}
func (c *Client) publish(subject string, body any, id string) error {
if c.Publish != nil {
return c.Publish(subject, body, id)
}
return stdio.SeatPublish(subject, body, id)
}
// Where is an ask's state as the router keeps it: its state and its warrant, if any — read under the
// module's own name and no other's.
func (c *Client) Where(id string) (state string, w *asks.Warrant, err error) {
read := c.Record
if read == nil {
read = func(k string) (*stdio.StateEntry, error) { return stdio.SeatRecord("", k) }
}
e, err := read(id)
if err != nil || e == nil {
return "", nil, err
}
var rec struct {
State string `json:"state"`
Warrant *asks.Warrant `json:"warrant"`
}
if err := json.Unmarshal(e.Value, &rec); err != nil {
return "", nil, fmt.Errorf("the router's record of %s cannot be read: %w", id, err)
}
return rec.State, rec.Warrant, nil
}
// Handled is what came of a warrant: acted on, refused (and why), or already done.
type Handled string
const (
Acted Handled = "acted"
Refused Handled = "refused"
Done Handled = "already acted on"
Recorded Handled = "ended without a choice"
)
// Take hands act a warrant once, after holding it to the ask it answers. act performs the option with the
// module's own grants; its error is said and the warrant stays acted on (a person's decision is not
// performed twice; the module says the failure in its own record).
func (c *Client) Take(w asks.Warrant, act func(asks.Option, asks.Warrant) error) (Handled, error) {
if w.Asker != c.Module {
return Refused, fmt.Errorf("a warrant for %s reached %s", w.Asker, c.Module)
}
e, err := c.State.Get(key(w.Ask))
if err != nil {
return "", err
}
if e == nil {
return Refused, fmt.Errorf("%s holds no ask %s", c.Module, w.Ask)
}
var k Kept
if err := json.Unmarshal(e.Value, &k); err != nil {
return "", err
}
if k.Acted || k.Ended != "" {
return Done, nil
}
if w.Outcome != asks.OutcomeChosen {
k.Ended = w.Outcome
k.Answer = &w
if _, err := c.State.Update(key(w.Ask), k, e.Revision); err != nil && !errors.Is(err, stdio.ErrChanged) {
return "", err
}
return Recorded, nil
}
o, err := w.For(c.Module, k.Ask)
if err != nil {
return Refused, err
}
// Marked before acting: of two deliveries, or two instances, only the one whose write stands acts.
k.Acted, k.Ended, k.Answer = true, asks.OutcomeChosen, &w
if _, err := c.State.Update(key(w.Ask), k, e.Revision); err != nil {
if errors.Is(err, stdio.ErrChanged) {
return Done, nil
}
return "", err
}
return Acted, act(o, w)
}
// Listen hands every warrant for this module to Take, with act. It asks the runtime for the module's
// events matching the router's `decided` subject for this module.
func (c *Client) Listen(act func(asks.Option, asks.Warrant) error, said func(Handled, asks.Warrant, error)) error {
subscribe := c.Subscribe
if subscribe == nil {
subscribe = stdio.Subscribe
}
return subscribe(asks.Seat+".decided."+c.Module, func(e stdio.Envelope) error {
if !strings.HasPrefix(e.Key, asks.Seat+".decided.") {
return nil
}
var w asks.Warrant
if err := json.Unmarshal(e.Body, &w); err != nil {
if said != nil {
said(Refused, w, fmt.Errorf("a warrant could not be read: %w", err))
}
return nil
}
h, err := c.Take(w, act)
if h == "" && err != nil {
return err // the state could not be reached: offered again
}
if said != nil {
said(h, w, err)
}
return nil
})
}