R-2: W1.9: a part's seat-list reader and its two tools are wired once
Four delivery parts each wrote the same wiring of the seat-list reader (start from the bootstrap, give the catalogue what it holds, read again when events go live, two tools over a reader set later). seatlist.Part is that wiring, its tools named by the part's prefix and its again answered to the callers the part gives (ADR 0636 §7), so W4-W8 delete their copies. Builds: design 47 G §204, §246 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
package seatlist
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync/atomic"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// A part that reads the seat list wires its reader one way (novox/hq ADR 0636 §1, §7): started from the bootstrapped
|
||||
// definitions, what it holds given to catalogue at once and at every change, read again when the part's path for the
|
||||
// seat's events becomes live, and its two tools `<prefix>_seat_list` and `<prefix>_seat_list_again` served over whatever
|
||||
// reader the part holds at the call. Part is that wiring, so no part writes it again; the part names its tools' prefix
|
||||
// and the callers that may read again, which are its own to give (stdio.VerbCallers' entries).
|
||||
|
||||
// Part is a part's reader of the seat list, set once Follow starts it; before, the list counts as unread and the part
|
||||
// acts on nothing new.
|
||||
type Part struct {
|
||||
prefix string
|
||||
callers []string
|
||||
reader atomic.Pointer[Reader]
|
||||
}
|
||||
|
||||
// NewPart is the wiring of a part whose tools are named `<prefix>_seat_list` and `<prefix>_seat_list_again`, the second
|
||||
// answered only to callers.
|
||||
func NewPart(prefix string, callers []string) *Part {
|
||||
return &Part{prefix: prefix, callers: append([]string(nil), callers...)}
|
||||
}
|
||||
|
||||
// Follow starts the reader over bus from the bootstrapped definitions: what it holds is given to catalogue at once and at
|
||||
// every change, and changed, when given, is called after each change (the part acts again). A reader that cannot
|
||||
// subscribe is said through logf and still held, from the bootstrap. It returns the reader.
|
||||
func (p *Part) Follow(bus Bus, changed func(), logf func(string, ...any)) *Reader {
|
||||
reader, err := Start(Config{Bus: bus, Bootstrap: Bootstrapped(),
|
||||
OnChange: func(seats []catalogue.Seat) {
|
||||
catalogue.UseSeats(seats)
|
||||
if changed != nil {
|
||||
changed()
|
||||
}
|
||||
}})
|
||||
if err != nil && logf != nil {
|
||||
logf("%s cannot follow its seat list, so it acts only on the seats the bootstrapped definitions name: %v", p.prefix, err)
|
||||
}
|
||||
if reader != nil {
|
||||
catalogue.UseSeats(reader.Seats())
|
||||
p.reader.Store(reader)
|
||||
}
|
||||
return reader
|
||||
}
|
||||
|
||||
// Reader is the reader Follow started, or nil before.
|
||||
func (p *Part) Reader() *Reader {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return p.reader.Load()
|
||||
}
|
||||
|
||||
// MayAct is the reader's MayAct, false before it is started.
|
||||
func (p *Part) MayAct(seats ...string) bool {
|
||||
r := p.Reader()
|
||||
return r != nil && r.MayAct(seats...)
|
||||
}
|
||||
|
||||
// Heard says the seat has answered the reader (Status's Heard), false before it is started.
|
||||
func (p *Part) Heard() bool {
|
||||
r := p.Reader()
|
||||
return r != nil && r.Status().Heard
|
||||
}
|
||||
|
||||
// Health is why the list is not read, or empty when it is.
|
||||
func (p *Part) Health() string {
|
||||
r := p.Reader()
|
||||
if r == nil {
|
||||
return "the seat list reader has not started yet"
|
||||
}
|
||||
return r.Status().Health()
|
||||
}
|
||||
|
||||
// EventsLive is called when the part's path for the seat's events is heard: while the list is unread it is read now, on a
|
||||
// goroutine of its own (Reader.ReloadIfUnread).
|
||||
func (p *Part) EventsLive() {
|
||||
if r := p.Reader(); r != nil {
|
||||
r.ReloadIfUnread()
|
||||
}
|
||||
}
|
||||
|
||||
// ToolNames are the two tools' names.
|
||||
func (p *Part) ToolNames() (read, again string) {
|
||||
return p.prefix + "_seat_list", p.prefix + "_seat_list_again"
|
||||
}
|
||||
|
||||
// Tools are the reader's two tools (Tools) over whatever reader the part holds at the call: before Follow, each says the
|
||||
// reader has not started.
|
||||
func (p *Part) Tools() []stdio.Tool {
|
||||
read, again := p.ToolNames()
|
||||
shape := Tools(&Reader{}, read, again, p.callers)
|
||||
for i := range shape {
|
||||
run := i
|
||||
shape[i].Run = func(args map[string]any) (any, error) {
|
||||
r := p.Reader()
|
||||
if r == nil {
|
||||
return nil, errors.New("the seat list reader has not started yet: the runtime has not served this module")
|
||||
}
|
||||
return Tools(r, read, again, p.callers)[run].Run(args)
|
||||
}
|
||||
}
|
||||
return shape
|
||||
}
|
||||
@@ -240,6 +240,40 @@ func TestTheTwoToolsReadTheHeldListAndReadItAgainForTheirCallers(t *testing.T) {
|
||||
if err := r.Reload(); err != nil || !r.Status().Heard || len(r.Status().Held) != len(Bootstrapped())+1 {
|
||||
t.Fatalf("again: %v %+v", err, r.Status())
|
||||
}
|
||||
|
||||
// W1.9: a part's wiring is one (Part): its two tools are named by its prefix and answer before the reader is started,
|
||||
// the list counts as unread then, and once followed what the reader holds is the catalogue's and a change wakes the part.
|
||||
saved := catalogue.Seats()
|
||||
t.Cleanup(func() { catalogue.UseSeats(saved) }) // Follow sets the catalogue's seats, a global; given back after
|
||||
part := NewPart("assigner", callers)
|
||||
if read, again := part.ToolNames(); read != "assigner_seat_list" || again != "assigner_seat_list_again" {
|
||||
t.Fatalf("tool names %s, %s", read, again)
|
||||
}
|
||||
before := part.Tools()
|
||||
if len(before) != 2 || before[0].Name != "assigner_seat_list" || before[1].Name != "assigner_seat_list_again" {
|
||||
t.Fatalf("tools %+v", before)
|
||||
}
|
||||
if _, err := before[0].Run(map[string]any{}); err == nil || !strings.Contains(err.Error(), "has not started") || part.Heard() ||
|
||||
part.MayAct("node-b") || part.Health() == "" {
|
||||
t.Fatalf("an unstarted part: %v, heard %v", err, part.Heard())
|
||||
}
|
||||
owner2 := &fakeOwner{table: map[string]string{"node-b": def("node-b", "node", "put")}}
|
||||
woken := make(chan bool, 4)
|
||||
pr := part.Follow(owner2, func() { woken <- true }, t.Logf)
|
||||
<-pr.Ready()
|
||||
if !part.Heard() || !part.MayAct("node-b") || part.Health() != pr.Status().Health() || part.Reader() != pr {
|
||||
t.Fatalf("a followed part: heard %v, health %q", part.Heard(), part.Health())
|
||||
}
|
||||
if _, ok := catalogue.SeatNamed("node-b"); !ok {
|
||||
t.Fatal("what the part's reader holds is not the catalogue's")
|
||||
}
|
||||
if _, err := part.Tools()[1].Run(map[string]any{}); err == nil || !strings.Contains(err.Error(), "it answers only operator, mesh-healers") {
|
||||
t.Fatalf("again without a proven caller: %v", err)
|
||||
}
|
||||
owner2.table["node-c"] = def("node-c", "node", "put")
|
||||
if err := owner2.say(ChangedEvent, "node-c", SourceTrunk); err != nil || len(woken) == 0 {
|
||||
t.Fatalf("a change did not wake the part: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// A reader started while the seat was silent is heard at the seat's changed event: the whole list is read then (ADR 0636 §1).
|
||||
|
||||
Reference in New Issue
Block a user