V-2: W1.13 (views adopt): a reader adopts a follower a seat's client made, so its callers write no glue

Six readers of the catalog half each wrote the same glue (dev-G-links' finding): the follower's conditions and
view-waits, the seat event's subscription, Keys/Get/Watch over its values and a wrap of the first-view tools. Reader.Adopt
and ProcessReader.Adopt take the client's follower with its seat's event and handler: the reader writes its findings
(unanswered, view-waits; Follow's followers now write view-waits too), reads and lists it as any first view, keeps its
own Changed after the reader's, and hears the seat's event (stdio: <seat>.<event>; process: the durable's
mesh.seat.<seat>.event.<event>, also on a shared hub) into the handler. A subtest of TestViews.

Builds: design 47 G §262, §260

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 10:51:07 +01:00
parent fa16c39057
commit 86ccaae967
3 changed files with 316 additions and 8 deletions
+104 -1
View File
@@ -14,6 +14,7 @@ import (
"encoding/json"
"errors"
"fmt"
"maps"
"slices"
"strings"
"sync"
@@ -55,6 +56,8 @@ type ProcessReader struct {
mu sync.Mutex
pics map[string]*Picture // by bucket name <owner>_<state>
order []string
// adopted are the handlers of the adopted followers' seat events (Adopt), by the event's subject.
adopted map[string]func(context.Context, []byte) error
}
func (r *ProcessReader) logf(format string, args ...any) {
@@ -101,6 +104,85 @@ func (r *ProcessReader) Follow(owner, state, tool string) (*Picture, error) {
return p, nil
}
// 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
// process'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 its picture (`<owner>_<state>`) hands each
// change on before the follower's own Changed, set before. Start keeps its status, hears its hand asks, and asks it with
// the others; its events are the seat's event (seatdef's EventSubject of seat and event, on the module's durable
// consumer, as its `consumes` names it), handed to hear, the client's handler, and acknowledged. Call it before Start;
// a state followed already is refused.
func (r *ProcessReader) Adopt(f *Follower, seat, event string, hear func(context.Context, []byte) error) (*Picture, error) {
if f == nil || f.Owner == "" || f.State == "" || seat == "" || event == "" || hear == nil {
return nil, errors.New("views: an adopted follower names its owner and state, and its seat's event and handler")
}
r.mu.Lock()
defer r.mu.Unlock()
if r.pics == nil {
r.pics = map[string]*Picture{}
}
bucket := f.Owner + "_" + f.State
if r.pics[bucket] != nil {
return nil, fmt.Errorf("views: %s's %s is followed already", f.Owner, f.State)
}
p := &Picture{bucket: bucket, ctx: context.Background(), f: f}
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
}
f.Raise, f.Clear = func(s Status) { r.finding(s, true) }, func(s Status) { r.finding(s, false) }
f.RaiseWaits, f.ClearWaits = func(s Status) { r.waits(s, true) }, func(s Status) { r.waits(s, false) }
own := f.Changed
f.Changed = func(key string, value json.RawMessage, deleted bool, at time.Time) {
p.changed(key, value, deleted, at)
if own != nil {
own(key, value, deleted, at)
}
}
r.pics[bucket] = p
r.order = append(r.order, bucket)
if r.adopted == nil {
r.adopted = map[string]func(context.Context, []byte) error{}
}
r.adopted["mesh.seat."+seat+".event."+event] = hear
return p, nil
}
// 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 *ProcessReader) 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
}
kv := r.Findings()
if kv == nil {
return
}
at := time.Now()
if r.Now != nil {
at = r.Now()
}
f := ViewWaitsFinding(r.Module, s, at)
var err error
if raised {
err = kv.Put(f)
} else {
err = kv.Delete(f)
}
if err != nil {
r.logf("views: the view-waits finding of %s is not written: %v", s.Tool, err)
}
}
// Picture is the picture of the bucket the process used to bind, by its bus name (`<owner>_<state>`); nil when not
// followed.
func (r *ProcessReader) Picture(bucket string) *Picture {
@@ -172,7 +254,17 @@ func (r *ProcessReader) Hear(ctx context.Context) func(jetstream.Msg) {
for owner, m := range fs {
views[owner] = Hear(ctx, m, r.logf)
}
r.mu.Lock()
adopted := maps.Clone(r.adopted)
r.mu.Unlock()
return func(m jetstream.Msg) {
if hear := adopted[m.Subject()]; hear != nil {
if err := hear(ctx, m.Data()); err != nil {
r.logf("views: %s is not taken: %v", m.Subject(), err)
}
_ = m.Ack()
return
}
if owner, ok := OwnerOfEvent(m.Subject()); ok {
if h := views[owner]; h != nil {
h(m)
@@ -550,7 +642,18 @@ func (r *ProcessReader) Start(ctx context.Context, js jetstream.JetStream, cred
f.ConsumerMade(ctx, created)
}
})
err := hub.Handle(pattern, r.Hear(ctx))
hear := r.Hear(ctx)
err := hub.Handle(pattern, hear)
if !own {
r.mu.Lock()
subjects := slices.Sorted(maps.Keys(r.adopted))
r.mu.Unlock()
for _, subject := range subjects {
if err == nil {
err = hub.Handle(subject, hear)
}
}
}
if err == nil && own {
err = hub.Start(ctx, js, cred)
}