Writer[F, C, S, A any] marshals a body, reads and sets kind, scope, id, subject, at, since and observer by their contract names; the interfaces FindingBody, ClearedBody, StatedBody, AskedBody and the constructors go. A body whose wire form lacks a field the contract names is refused when the writer is made. views.CheckFinding(any) judges the wire form. WriterFindings takes only the writer. Builds: design 47 G §686, §640, §687, §689, A-3 (.work/47-G/dev2-ledger.md) Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
670 lines
23 KiB
Go
670 lines
23 KiB
Go
package views
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
"git.novox.be/novox/mesh-sdk/go/keys"
|
|
)
|
|
|
|
// Reader is a module's pictures of the owners' states it used to read (novox/hq ADR 0615 §1): it
|
|
// asks each owner's read tool for the first view, subscribes the owner's views family, and answers the module's own
|
|
// `Keys` and `Get` of a state from the picture, by the name the module's `reads` used to carry (`<owner>.<state>`).
|
|
// A module in a stdio tools bundle declares `invokes` for the tools and `consumes` for the events, and names no `reads`
|
|
// of these owners any more. Pictures are kept in memory only: the owner's state is the truth, and a restart asks again.
|
|
type Reader struct {
|
|
// Module is the reading module, named in the findings it writes.
|
|
Module string
|
|
// Findings writes and clears this module's finding of an unanswered first view (its own state `findings`); nil
|
|
// says it in the log only.
|
|
Findings FindingWriter
|
|
Logf func(string, ...any)
|
|
// Changed is told of each key a followed state changes, after the picture holds it (state is `<owner>.<state>`).
|
|
Changed func(state, key string, value json.RawMessage, deleted bool)
|
|
// Acts records a re-read by hand before it is asked (nil records nothing
|
|
// and the re-read is refused).
|
|
Acts HandActs
|
|
// Now, Bound and Wait are for tests: zero are the clock, Bound and five seconds.
|
|
Now func() time.Time
|
|
Bound time.Duration
|
|
Wait time.Duration
|
|
|
|
mu sync.Mutex
|
|
followers map[string]*Follower // by "<owner>.<state>"
|
|
order []string
|
|
byOwner map[string]map[string]*Follower // owner -> state -> follower
|
|
watchers map[string][]func(key string, value []byte, gone bool)
|
|
// adopted are the followers a seat's client made (Adopt), by "<owner>.<state>": their events are a seat's, heard by
|
|
// the client's own handler, not an owner's views family.
|
|
adopted map[string]adoption
|
|
// events is, of each per-branch view (FollowBranch), by "<owner>.<state>.<branch>", the state as its owner's events name it (§521).
|
|
events map[string]string
|
|
|
|
// After Start (design 47 G §532): the reader's life, the event patterns whose subscription is taken (heard) or being
|
|
// asked (asking), and of each first view begun, the cancel of its asks, by "<owner>.<state>".
|
|
ctx context.Context
|
|
started bool
|
|
heard map[string]bool
|
|
asking map[string]bool
|
|
begun map[string]context.CancelFunc
|
|
}
|
|
|
|
// subscribe is how the reader asks the runtime for an event pattern; a variable so a test hears without a runtime.
|
|
var subscribe = stdio.Subscribe
|
|
|
|
// adoption is how an adopted follower's events are heard: the seat's event, by its local name, and the client's handler.
|
|
type adoption struct {
|
|
seat, event string
|
|
hear func(context.Context, []byte) error
|
|
}
|
|
|
|
// local is an adopted follower's event as the stdio runtime subscribes it: one subscription per event with every
|
|
// further part, `<seat>.<event>.>` (design 47 G §532, §521).
|
|
func (a adoption) local() string { return a.seat + "." + a.event + ".>" }
|
|
|
|
func (r *Reader) logf(format string, args ...any) {
|
|
if r.Logf != nil {
|
|
r.Logf(format, args...)
|
|
}
|
|
}
|
|
|
|
// Follow adds a state of an owner to follow, asked through the owner's read tool (`<module>.<tool>`, as the reader's
|
|
// `invokes` names it), with `values` so the answer is the first view. Called after Start, the view takes its first view
|
|
// at once and then follows (design 47 G §532).
|
|
func (r *Reader) Follow(owner, state, tool string) *Follower {
|
|
f := r.follow(owner, state, tool, "")
|
|
r.begin(owner + "." + f.State)
|
|
return f
|
|
}
|
|
|
|
// follow is Follow without beginning it: event, when given, is the state as the owner's events name it (FollowBranch).
|
|
func (r *Reader) follow(owner, state, tool, event string) *Follower {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if r.followers == nil {
|
|
r.followers, r.byOwner = map[string]*Follower{}, map[string]map[string]*Follower{}
|
|
}
|
|
name := owner + "." + state
|
|
f := &Follower{Owner: owner, Tool: tool, State: state, Logf: r.Logf, Now: r.Now, Bound: r.Bound, Wait: r.Wait}
|
|
f.Ask = func(_ context.Context, after string) (Answer, error) {
|
|
body := map[string]any{"values": true}
|
|
if after != "" {
|
|
body["after"] = after
|
|
}
|
|
raw, err := stdio.Ask(tool, body)
|
|
if err != nil {
|
|
return Answer{}, err
|
|
}
|
|
return DecodeAnswer(raw)
|
|
}
|
|
r.wireLocked(name, f, nil)
|
|
if event != "" {
|
|
if r.events == nil {
|
|
r.events = map[string]string{}
|
|
}
|
|
r.events[name] = event
|
|
}
|
|
return f
|
|
}
|
|
|
|
// BranchState is the name of a per-branch state's view: `<state>.<branch>`, the branch escaped as a key's name is (design
|
|
// 47 G §479), so a reader names the view `<owner>.<state>.<branch>`. A state that is not per branch keeps its name.
|
|
func BranchState(state, branch string) string { return state + "." + keys.EscapeName(branch) }
|
|
|
|
// FollowBranch is Follow for a state kept per branch: the view of one branch is named `<owner>.<state>.<branch>`
|
|
// (BranchState), so a reader follows several branches of one state, each its own view and its own row of
|
|
// `<module>_first_views` (design 47 G §479). Called after Start, the view takes its first view at once and then follows
|
|
// (§532).
|
|
func (r *Reader) FollowBranch(owner, state, branch, tool string) *Follower {
|
|
f := r.follow(owner, BranchState(state, branch), tool, state)
|
|
r.begin(owner + "." + f.State)
|
|
return f
|
|
}
|
|
|
|
// Adopt takes a follower a seat's client made (its Ask a seat's verb, such as the catalog half's view) as one of the
|
|
// reader's first views, so the client's callers write no glue: the reader sets its conditions (the unanswered first view
|
|
// and view-waits, in the reader's findings), its log and clock, and hands each change to Watch, Changed and the
|
|
// follower's own Changed, set before; Keys, Get, Values, Watch, Statuses and the two tools name it `<owner>.<state>`.
|
|
// Its events are the seat's event (`<seat>.<event>`, as the reader's `consumes` names it), which Start subscribes and
|
|
// hands to hear, the client's handler; a state followed already is refused. Adopted after Start, as a part adopts a
|
|
// watched branch added while it runs, the view takes its first view at once and then follows (design 47 G §532).
|
|
func (r *Reader) Adopt(f *Follower, seat, event string, hear func(context.Context, []byte) error) error {
|
|
if f == nil || f.Owner == "" || f.State == "" || seat == "" || event == "" || hear == nil {
|
|
return errors.New("views: an adopted follower names its owner and state, and its seat's event and handler")
|
|
}
|
|
if err := r.adopt(f, seat, event, hear); err != nil {
|
|
return err
|
|
}
|
|
r.begin(f.Owner + "." + f.State)
|
|
return nil
|
|
}
|
|
|
|
func (r *Reader) adopt(f *Follower, seat, event string, hear func(context.Context, []byte) error) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if r.followers == nil {
|
|
r.followers, r.byOwner = map[string]*Follower{}, map[string]map[string]*Follower{}
|
|
}
|
|
name := f.Owner + "." + f.State
|
|
if r.followers[name] != nil {
|
|
return fmt.Errorf("views: %s is followed already", name)
|
|
}
|
|
if f.Logf == nil {
|
|
f.Logf = r.Logf
|
|
}
|
|
if f.Now == nil {
|
|
f.Now = r.Now
|
|
}
|
|
if f.Bound == 0 {
|
|
f.Bound = r.Bound
|
|
}
|
|
if f.Wait == 0 {
|
|
f.Wait = r.Wait
|
|
}
|
|
r.wireLocked(name, f, f.Changed)
|
|
if r.adopted == nil {
|
|
r.adopted = map[string]adoption{}
|
|
}
|
|
r.adopted[name] = adoption{seat: seat, event: event, hear: hear}
|
|
return nil
|
|
}
|
|
|
|
// wireLocked makes f one of the reader's first views under name: its conditions are the reader's, and each change goes
|
|
// to the watchers, Changed and own (the follower's own Changed, or nil). The caller holds mu.
|
|
func (r *Reader) wireLocked(name string, f *Follower, own func(string, json.RawMessage, bool, time.Time)) {
|
|
// A view dropped writes no finding after its drop cleared them (Drop).
|
|
held := func() bool { r.mu.Lock(); defer r.mu.Unlock(); return r.followers[name] == f }
|
|
f.Raise = func(s Status) {
|
|
if held() {
|
|
r.raise(s)
|
|
}
|
|
}
|
|
f.Clear = func(s Status) { r.clear(s) }
|
|
f.RaiseWaits = func(s Status) {
|
|
if held() {
|
|
r.waits(s, true)
|
|
}
|
|
}
|
|
f.ClearWaits = func(s Status) { r.waits(s, false) }
|
|
f.Changed = func(key string, value json.RawMessage, deleted bool, at time.Time) {
|
|
r.mu.Lock()
|
|
ws := append([]func(string, []byte, bool){}, r.watchers[name]...)
|
|
r.mu.Unlock()
|
|
for _, w := range ws {
|
|
w(key, value, deleted)
|
|
}
|
|
if r.Changed != nil {
|
|
r.Changed(name, key, value, deleted)
|
|
}
|
|
if own != nil {
|
|
own(key, value, deleted, at)
|
|
}
|
|
}
|
|
r.followers[name] = f
|
|
r.order = append(r.order, name)
|
|
if r.byOwner[f.Owner] == nil {
|
|
r.byOwner[f.Owner] = map[string]*Follower{}
|
|
}
|
|
r.byOwner[f.Owner][f.State] = f
|
|
}
|
|
|
|
// waits writes (raised) or clears the reader's view-waits finding of s (design 47 *A part with no view says it waits*).
|
|
func (r *Reader) waits(s Status, raised bool) {
|
|
if raised {
|
|
r.logf("%s has waited for %s's view %s since %s", r.Module, s.Owner, s.WaitsFor, s.WaitingSince.Format(time.RFC3339))
|
|
}
|
|
if r.Findings == nil {
|
|
return
|
|
}
|
|
f := ViewWaitsFinding(r.Module, s, r.nowTime())
|
|
var err error
|
|
if raised {
|
|
err = r.Findings.Put(f)
|
|
} else {
|
|
err = r.Findings.Delete(f)
|
|
}
|
|
if err != nil {
|
|
r.logf("the view-waits finding of %s is not written: %v", s.Tool, err)
|
|
}
|
|
}
|
|
|
|
func (r *Reader) nowTime() time.Time {
|
|
if r.Now != nil {
|
|
return r.Now().UTC()
|
|
}
|
|
return time.Now().UTC()
|
|
}
|
|
|
|
func (r *Reader) raise(s Status) {
|
|
r.logf("the first view %s of %s's %s is unanswered past its bound: %s", s.Tool, s.Owner, s.State, s.Error)
|
|
if r.Findings == nil {
|
|
return
|
|
}
|
|
if err := r.Findings.Put(UnansweredFinding(r.Module, s, r.nowTime())); err != nil {
|
|
r.logf("the finding of the unanswered first view %s could not be written: %v", s.Tool, err)
|
|
}
|
|
}
|
|
|
|
func (r *Reader) clear(s Status) {
|
|
if r.Findings == nil {
|
|
return
|
|
}
|
|
if err := r.Findings.Delete(UnansweredFinding(r.Module, s, r.nowTime())); err != nil {
|
|
r.logf("the finding of the first view %s could not be cleared: %v", s.Tool, err)
|
|
}
|
|
}
|
|
|
|
// Start subscribes each owner's views family (the module's `consumes`: `<owner>.views.<state>`) and asks every first
|
|
// view until it is answered, each on a goroutine of its own, so a module starts at once and a reader whose owner is down
|
|
// waits, saying so (ADR 0615 §6). It returns when the subscriptions are asked. A view followed or adopted after it is
|
|
// begun the same way, and Drop ends one (design 47 G §532).
|
|
func (r *Reader) Start(ctx context.Context) {
|
|
r.mu.Lock()
|
|
r.ctx, r.started = ctx, true
|
|
names := append([]string(nil), r.order...)
|
|
r.mu.Unlock()
|
|
for _, name := range names {
|
|
r.begin(name)
|
|
}
|
|
}
|
|
|
|
// patternLocked is the event pattern a followed state's events come on: one subscription per pattern, the followers of
|
|
// a per-branch state sharing the owner's one event, which HearEvent routes to the branch's view by the key's first token
|
|
// (§521). The caller holds mu.
|
|
func (r *Reader) patternLocked(name string) string {
|
|
if a, ok := r.adopted[name]; ok {
|
|
return a.local()
|
|
}
|
|
f := r.followers[name]
|
|
if state, ok := r.events[name]; ok {
|
|
return f.Owner + "." + EventName(state)
|
|
}
|
|
return f.Owner + "." + EventName(f.State)
|
|
}
|
|
|
|
// begin starts the first view of name once the reader started: at once when its event pattern is heard, else once its
|
|
// subscription is taken, which it asks when none is asked yet, so no event said after the first view is missed.
|
|
func (r *Reader) begin(name string) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if !r.started || r.followers[name] == nil {
|
|
return
|
|
}
|
|
pattern := r.patternLocked(name)
|
|
if r.heard[pattern] {
|
|
r.askLocked(name)
|
|
return
|
|
}
|
|
if r.asking[pattern] {
|
|
return
|
|
}
|
|
if r.asking == nil {
|
|
r.asking, r.heard = map[string]bool{}, map[string]bool{}
|
|
}
|
|
r.asking[pattern] = true
|
|
ctx, f := r.ctx, r.followers[name]
|
|
handle := r.handler(ctx, pattern)
|
|
go func() {
|
|
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
|
|
err := subscribe(pattern, handle)
|
|
if err == nil {
|
|
break
|
|
}
|
|
r.logf("not hearing %s yet for %s's %s (%v); asking again in %s", pattern, f.Owner, f.State, err, wait)
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-time.After(wait):
|
|
}
|
|
}
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.heard[pattern] = true
|
|
for _, n := range r.order {
|
|
if r.patternLocked(n) == pattern {
|
|
r.askLocked(n)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// askLocked asks name's first view on a goroutine of its own, once, until answered or the view is dropped. The caller
|
|
// holds mu.
|
|
func (r *Reader) askLocked(name string) {
|
|
if r.begun[name] != nil {
|
|
return
|
|
}
|
|
if r.begun == nil {
|
|
r.begun = map[string]context.CancelFunc{}
|
|
}
|
|
ctx, cancel := context.WithCancel(r.ctx)
|
|
r.begun[name] = cancel
|
|
f := r.followers[name]
|
|
go func() {
|
|
if err := f.Start(ctx); err != nil && ctx.Err() == nil {
|
|
r.logf("the first view %s was not answered: %v", f.Tool, err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
// handler is the handler of one event pattern: the followers on it are read at each event, so a view adopted after the
|
|
// subscription hears it and a dropped one no longer does. An event the adopted followers on it hear goes to each
|
|
// client's handler; otherwise HearEvent routes it to its state's view.
|
|
func (r *Reader) handler(ctx context.Context, pattern string) func(stdio.Envelope) error {
|
|
return func(e stdio.Envelope) error {
|
|
r.mu.Lock()
|
|
var fs []*Follower
|
|
for _, n := range r.order {
|
|
if r.patternLocked(n) == pattern {
|
|
fs = append(fs, r.followers[n])
|
|
}
|
|
}
|
|
adopted := maps.Clone(r.adopted)
|
|
r.mu.Unlock()
|
|
if h := r.hearAdoptedBy(ctx, fs, adopted); h != nil {
|
|
return h(e)
|
|
}
|
|
return r.hear(ctx, fs)(e)
|
|
}
|
|
}
|
|
|
|
// Drop ends the view of state (`<owner>.<state>`), as a part drops a watched branch removed while it runs (design 47 G
|
|
// §532, §614): it asks it no more and hears none of its events, its row leaves `<module>_first_views`, and every finding
|
|
// it raised (its unanswered first view, its view-waits) is cleared through the reader's findings, the drop and each
|
|
// cleared finding said in the log. A state not followed is an error.
|
|
func (r *Reader) Drop(state string) error {
|
|
r.mu.Lock()
|
|
f := r.followers[state]
|
|
if f == nil {
|
|
r.mu.Unlock()
|
|
return fmt.Errorf("%s is not a state this module follows", state)
|
|
}
|
|
cancel := r.begun[state]
|
|
delete(r.followers, state)
|
|
delete(r.begun, state)
|
|
delete(r.adopted, state)
|
|
delete(r.events, state)
|
|
delete(r.watchers, state)
|
|
r.order = slices.DeleteFunc(r.order, func(n string) bool { return n == state })
|
|
if byState := r.byOwner[f.Owner]; byState[f.State] == f {
|
|
delete(byState, f.State)
|
|
}
|
|
r.mu.Unlock()
|
|
if cancel != nil {
|
|
cancel()
|
|
}
|
|
r.logf("views: %s drops its view %s", r.Module, state)
|
|
s := f.Status()
|
|
if s.Raised {
|
|
r.clear(s)
|
|
r.logf("views: %s clears its finding %s of the dropped view %s", r.Module, UnansweredKind, state)
|
|
}
|
|
if s.WaitsRaised {
|
|
r.waits(s, false)
|
|
r.logf("views: %s clears its finding %s of the dropped view %s", r.Module, ViewWaitsKind, state)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// hear is the handler of one followed state's events: HearEvent routes it, and a gap asks that first view once more as a
|
|
// gap, said in the log.
|
|
func (r *Reader) hear(ctx context.Context, fs []*Follower) func(stdio.Envelope) error {
|
|
byState := map[string]*Follower{}
|
|
for _, f := range fs {
|
|
byState[f.State] = f
|
|
}
|
|
return func(e stdio.Envelope) error {
|
|
if err := HearEvent(ctx, byState, e.Body, r.logf); err != nil {
|
|
r.logf("views: %s is not a views event, dropped: %v", e.Key, err)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// hearAdoptedBy is the handler of an event every adopted follower of fs hears, never the first alone (two branches' views
|
|
// of the catalog half share one event, design 47 G §532); nil when none of fs is adopted.
|
|
func (r *Reader) hearAdoptedBy(ctx context.Context, fs []*Follower, adopted map[string]adoption) func(stdio.Envelope) error {
|
|
var heard []func(stdio.Envelope) error
|
|
for _, f := range fs {
|
|
if a, ok := adopted[f.Owner+"."+f.State]; ok {
|
|
heard = append(heard, r.hearAdopted(ctx, a))
|
|
}
|
|
}
|
|
if len(heard) == 0 {
|
|
return nil
|
|
}
|
|
return func(e stdio.Envelope) error {
|
|
for _, h := range heard {
|
|
_ = h(e)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// hearAdopted is the handler of an adopted follower's seat event: the client's handler takes it, and what it cannot read is
|
|
// said in the log.
|
|
func (r *Reader) hearAdopted(ctx context.Context, a adoption) func(stdio.Envelope) error {
|
|
return func(e stdio.Envelope) error {
|
|
if err := a.hear(ctx, e.Body); err != nil {
|
|
r.logf("views: %s is not taken: %v", e.Key, err)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Ready is closed once every followed state had its first view.
|
|
func (r *Reader) Ready() <-chan struct{} {
|
|
r.mu.Lock()
|
|
fs := make([]*Follower, 0, len(r.followers))
|
|
for _, name := range r.order {
|
|
fs = append(fs, r.followers[name])
|
|
}
|
|
r.mu.Unlock()
|
|
done := make(chan struct{})
|
|
go func() {
|
|
for _, f := range fs {
|
|
<-f.Ready()
|
|
}
|
|
close(done)
|
|
}()
|
|
return done
|
|
}
|
|
|
|
// Keys is the keys of a followed state (`<owner>.<state>`); a state not followed is an error, never a read of it.
|
|
func (r *Reader) Keys(state string) ([]string, error) {
|
|
f, err := r.picture(state)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
vals := f.Values()
|
|
keys := make([]string, 0, len(vals))
|
|
for k := range vals {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys, nil
|
|
}
|
|
|
|
// Get is one key of a followed state.
|
|
func (r *Reader) Get(state, key string) ([]byte, bool, error) {
|
|
f, err := r.picture(state)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
v, ok := f.Get(key)
|
|
return v, ok, nil
|
|
}
|
|
|
|
// Values is every key of a followed state, by key.
|
|
func (r *Reader) Values(state string) (map[string][]byte, error) {
|
|
f, err := r.picture(state)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := map[string][]byte{}
|
|
for k, v := range f.Values() {
|
|
out[k] = v
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r *Reader) picture(state string) (*Follower, error) {
|
|
r.mu.Lock()
|
|
f := r.followers[state]
|
|
r.mu.Unlock()
|
|
if f == nil {
|
|
return nil, fmt.Errorf("%s is not a state this module follows", state)
|
|
}
|
|
select {
|
|
case <-f.Ready():
|
|
return f, nil
|
|
default:
|
|
return nil, fmt.Errorf("no first view of %s yet: %s has not answered", state, f.Tool)
|
|
}
|
|
}
|
|
|
|
// Watch is a followed state as a module's store watches one: it returns once the first view was answered and every
|
|
// current value was handed to handle, and then hands every change for as long as the process runs (gone is a key
|
|
// deleted). A change heard while the current values are handed over may be handed twice; a handler is idempotent.
|
|
func (r *Reader) Watch(state string, handle func(key string, value []byte, gone bool)) error {
|
|
r.mu.Lock()
|
|
f := r.followers[state]
|
|
if f == nil {
|
|
r.mu.Unlock()
|
|
return fmt.Errorf("%s is not a state this module follows", state)
|
|
}
|
|
r.mu.Unlock()
|
|
<-f.Ready()
|
|
r.mu.Lock()
|
|
if r.watchers == nil {
|
|
r.watchers = map[string][]func(string, []byte, bool){}
|
|
}
|
|
r.watchers[state] = append(r.watchers[state], handle)
|
|
r.mu.Unlock()
|
|
for k, v := range f.Values() {
|
|
handle(k, v, false)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Follows says whether the state is one this reader follows.
|
|
func (r *Reader) Follows(state string) bool {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
return r.followers[state] != nil
|
|
}
|
|
|
|
// Statuses is each followed state's status, in the order they were added.
|
|
func (r *Reader) Statuses() []Status {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
out := make([]Status, 0, len(r.order))
|
|
for _, name := range r.order {
|
|
out = append(out, r.followers[name].Status())
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Tools are the reader's two tools (design 25 § 12.2): `<prefix>_first_views` says where each first view stands and
|
|
// `<prefix>_first_view_again` asks one again by hand (a hand act, counted in the status).
|
|
func (r *Reader) Tools(prefix string) []stdio.Tool {
|
|
return []stdio.Tool{
|
|
{Name: prefix + "_first_views",
|
|
Description: "Where each first view this module keeps stands (novox/hq ADR 0615): the owner and its tool, the revision the picture holds, when it was asked and answered, how many asks (by hand among them) and whether its condition is raised. (r)",
|
|
Run: func(map[string]any) (any, error) { return map[string]any{"views": r.Statuses()}, nil }},
|
|
{Name: prefix + "_first_view_again",
|
|
Description: "Ask a first view again by hand, for the state named (<owner>.<state>), or every one with none: a hand act, for a picture the operator doubts. Answers where it stands after. (a)",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{
|
|
"state": map[string]any{"type": "string", "description": "<owner>.<state>; none asks every first view"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
want, _ := args["state"].(string)
|
|
want = strings.TrimSpace(want)
|
|
r.mu.Lock()
|
|
var fs []*Follower
|
|
for _, name := range r.order {
|
|
if want == "" || want == name {
|
|
fs = append(fs, r.followers[name])
|
|
}
|
|
}
|
|
r.mu.Unlock()
|
|
if len(fs) == 0 {
|
|
return nil, fmt.Errorf("%q is not a state this module follows", want)
|
|
}
|
|
// A hand act, recorded before it is done; one that cannot be recorded is
|
|
// said and nothing is asked.
|
|
why := "the operator asked"
|
|
if err := recordHandAct(r.Acts, HandAct{What: "asked a first view again by hand (" + r.Module + ")", Why: why,
|
|
Cause: "first-view-again", Verb: "first-view-again"}); err != nil {
|
|
return nil, fmt.Errorf("the hand act was not recorded, so nothing was asked again: %w", err)
|
|
}
|
|
for _, f := range fs {
|
|
if err := f.Again(context.Background(), why); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return map[string]any{"views": r.Statuses()}, nil
|
|
}},
|
|
}
|
|
}
|
|
|
|
// FindingScope is the scope of a views reader's findings: they are findings of the part's module (design 47 G §687).
|
|
const FindingScope = "module"
|
|
|
|
// wireWriter is the part of a Writer[F, C, S, A] a views reader's findings use, whatever the contract's body types.
|
|
type wireWriter interface {
|
|
WriteWire(ctx context.Context, finding map[string]any) error
|
|
Clear(ctx context.Context, scope, id, kind, why string) error
|
|
ModuleNode() (module, node string)
|
|
}
|
|
|
|
// WriterFindings writes a views reader's findings through the part's mesh-sdk writer, as kinds of the reader's own
|
|
// (design 47 G §639, §580, §687): the Findings of a reader, never a writer of its own. mesh-sdk lays each finding out on
|
|
// its wire form: `kind` the finding's, `scope` module, `id` the finding's id escaped as a key's name is, `severity`
|
|
// warning but urgent for view-too-large, `source` views, `subject` the part's module on its node, `summary` and
|
|
// `headline` the finding's texts. The writer's statement covers the reader's open findings beside its observe's. Writer
|
|
// is the part's *Writer[F, C, S, A].
|
|
type WriterFindings struct {
|
|
Writer wireWriter
|
|
}
|
|
|
|
func (o WriterFindings) body(f Finding) map[string]any {
|
|
module, node := o.Writer.ModuleNode()
|
|
sev := "warning"
|
|
if f.Kind == TooLargeKind {
|
|
sev = "urgent"
|
|
}
|
|
subject := map[string]any{"scope": FindingScope, "id": module}
|
|
if node != "" {
|
|
subject["machine"] = node
|
|
}
|
|
b := map[string]any{"kind": f.Kind, "scope": FindingScope, "id": keys.EscapeName(f.ID), "severity": sev, "source": "views",
|
|
"subject": subject, "summary": f.Summary}
|
|
if f.Headline != "" {
|
|
b["headline"] = f.Headline
|
|
}
|
|
if !f.At.IsZero() {
|
|
b["at"] = f.At
|
|
}
|
|
return b
|
|
}
|
|
|
|
// Put writes one finding.
|
|
func (o WriterFindings) Put(f Finding) error {
|
|
return o.Writer.WriteWire(context.Background(), o.body(f))
|
|
}
|
|
|
|
// Delete clears one finding.
|
|
func (o WriterFindings) Delete(f Finding) error {
|
|
b := o.body(f)
|
|
return o.Writer.Clear(context.Background(), FindingScope, b["id"].(string), f.Kind, "the part no longer observes it")
|
|
}
|