197 lines
6.4 KiB
Go
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
|
|
})
|
|
}
|