Revert the revert of G's mesh-sdk merge (#191): G's sdk stands again over reader release 3

Main reverted G's half merge (#189, reverted by 8afc3eb in #191) and then took the reader release's S1-S5, which
copied G's code. G's form holds where they differ (process_bus.go, seats.go, state.go and their tests); the
release's kept names stay so main's callers keep building: SeatViewPrefix beside SeatReadPrefix, and QueueInvoke's
empty strings on not-ok (with its test TestQueueInvoke). The tree differs from G's head f30dee54 only by those and
the release's two tests (objects cap option, seat states and stream kinds).

Builds: design 47 G (the milestone's sdk part, as at f30dee54)

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-13 12:49:56 +01:00
parent 93e0a7b50d
commit 5fa64be894
211 changed files with 32027 additions and 2484 deletions
+369 -37
View File
@@ -14,6 +14,7 @@ import (
"encoding/json"
"errors"
"fmt"
"maps"
"slices"
"strings"
"sync"
@@ -35,7 +36,7 @@ type ProcessReader struct {
Call func(key string, body any) (json.RawMessage, error)
// Findings is the reader's own state `findings`, where the condition keeper raises and clears the condition of a
// first view unanswered past its bound (ADR 0615 §6); nil says it in the log only.
Findings func() FindingWriter
Findings func() WireWriter
Logf func(string, ...any)
// Now, Bound and Wait are for tests: zero are the clock, Bound and five seconds.
Now func() time.Time
@@ -55,6 +56,29 @@ 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.
// Several followers adopt one event (two branches' views of the catalog half), each hearing it (design 47 G §532).
adopted map[string][]adoptedHear
// seats are the views found by their seat (FollowSeat), by their picture's bus name <seat>_<state> (design 47 G §717).
seats map[string]*seatFollow
// After Start and Ask (design 47 G §532): the hub the reader's events come through when it is the module's (Hub) and
// its handler, the time the bus made the consumer, the asks' life and Said, and of each first view begun, the cancel
// of its asks, by bucket; status is the state `first-views` each status is kept in.
hub *process.EventHub
handle func(jetstream.Msg)
created time.Time
ctx context.Context
said func(Status)
asked bool
begun map[string]context.CancelFunc
status *process.AwaitedBucket
}
// adoptedHear is one adopted follower's handler of its seat event, by its picture's bucket.
type adoptedHear struct {
bucket string
hear func(context.Context, []byte) error
}
func (r *ProcessReader) logf(format string, args ...any) {
@@ -66,19 +90,28 @@ func (r *ProcessReader) logf(format string, args ...any) {
// Follow adds the owner's state to follow, as the owner declares it (design 25 § 12.2): tool is its ordinary read tool
// of state as the reader's `invokes` names it (`<owner>.<tool>`, from the owner's manifest's tools). The owner's changes
// are ordinary events (`<owner>.views.<state>`), which the reader's manifest `consumes` one entry per followed state.
// views names no owner: the caller gives it. Call it before Start.
// views names no owner: the caller gives it. Called after Start, the view takes its first view at once and then follows
// (design 47 G §532).
func (r *ProcessReader) Follow(owner, state, tool string) (*Picture, error) {
p, fresh, err := r.follow(owner, state, tool)
if err == nil && fresh {
r.begin(p.bucket)
}
return p, err
}
func (r *ProcessReader) follow(owner, state, tool string) (*Picture, bool, error) {
r.mu.Lock()
defer r.mu.Unlock()
if owner == "" || state == "" || !strings.HasPrefix(tool, owner+".") {
return nil, fmt.Errorf("%s follows %s's %s by its read tool <owner>.<tool>, not %q", r.Module, owner, state, tool)
return nil, false, fmt.Errorf("%s follows %s's %s by its read tool <owner>.<tool>, not %q", r.Module, owner, state, tool)
}
if r.pics == nil {
r.pics = map[string]*Picture{}
}
bucket := owner + "_" + state
if p := r.pics[bucket]; p != nil {
return p, nil
return p, false, nil
}
p := &Picture{bucket: bucket, ctx: context.Background()}
call := r.Call
@@ -94,13 +127,173 @@ func (r *ProcessReader) Follow(owner, state, tool string) (*Picture, error) {
return call(key, m)
}, tool)
p.f = &Follower{Owner: owner, Tool: tool, State: state, Ask: ask, Logf: r.Logf, Now: r.Now, Bound: r.Bound, Wait: r.Wait,
Raise: func(s Status) { r.finding(s, true) }, Clear: func(s Status) { r.finding(s, false) },
Changed: p.changed}
Raise: func(s Status) {
if r.holds(p) {
r.finding(s, true)
}
}, Clear: func(s Status) { r.finding(s, false) },
Changed: func(key string, value json.RawMessage, deleted bool, at time.Time) {
p.changed(key, value, deleted, at)
r.seatsChanged() // a seat's view reads its holder again (§717)
}}
r.pics[bucket] = p
r.order = append(r.order, bucket)
return p, true, nil
}
// holds says whether p is still a picture of the reader: a dropped view writes no finding after its drop cleared them.
func (r *ProcessReader) holds(p *Picture) bool {
r.mu.Lock()
defer r.mu.Unlock()
return r.pics[p.bucket] == p
}
// FollowBranch is Follow for a state kept per branch: the view of one branch is named `<owner>.<state>.<branch>` (the
// branch escaped as a key's name is, BranchState) and its picture `<owner>_<state>.<branch>`, so a process follows
// several branches of one state, each its own view (design 47 G §479). Called after Start, it begins at once (§532).
func (r *ProcessReader) FollowBranch(owner, state, branch, tool string) (*Picture, error) {
return r.Follow(owner, BranchState(state, branch), tool)
}
// 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; 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 *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")
}
p, subject, err := r.adopt(f, seat, event, hear)
if err != nil {
return nil, err
}
r.mu.Lock()
hub, handle := r.hub, r.handle
r.mu.Unlock()
if subject != "" && hub != nil {
// one subscription per event, with every further part (design 47 G §532)
if err := hub.Handle(subject+".>", handle); err != nil {
r.logf("views: %s's event %s is not heard (%v): the view %s is its first view's alone, asked again by hand", r.Module, subject, err, p.bucket)
}
}
r.begin(p.bucket)
return p, nil
}
// AdoptEvent hears a seat's event on the one subscription the followers adopted on that event share, with no picture and no
// first view (design 47 G A-185, A-199): hear is handed each message of the event beside every adopted follower's handler,
// and the message is acknowledged, unless hear fails: then it is left unacknowledged and the bus asks again (A-222). The subscription is made at once when no follower made it, so a part that follows no
// view of the seat still hears the event; adopted before Start, it is made at Start.
func (r *ProcessReader) AdoptEvent(seat, event string, hear func(context.Context, []byte) error) error {
if seat == "" || event == "" || hear == nil {
return errors.New("views: an adopted event names its seat, its event and its handler")
}
subject := "mesh.seat." + seat + ".event." + event
r.mu.Lock()
if r.adopted == nil {
r.adopted = map[string][]adoptedHear{}
}
fresh := len(r.adopted[subject]) == 0
r.adopted[subject] = append(r.adopted[subject], adoptedHear{hear: hear})
hub, handle := r.hub, r.handle
r.mu.Unlock()
if fresh && hub != nil {
// one subscription per event, with every further part (design 47 G §532)
if err := hub.Handle(subject+".>", handle); err != nil {
r.logf("views: %s's event %s is not heard (%v)", r.Module, subject, err)
}
}
return nil
}
// adopt is Adopt's part under the lock; subject is the event's subject when no follower adopted it before.
func (r *ProcessReader) adopt(f *Follower, seat, event string, hear func(context.Context, []byte) error) (*Picture, string, error) {
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) {
if r.holds(p) {
r.finding(s, true)
}
}, func(s Status) { r.finding(s, false) }
f.RaiseWaits, f.ClearWaits = func(s Status) {
if r.holds(p) {
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.seatsChanged() // a seat's view reads its holder again (§717)
}
r.pics[bucket] = p
r.order = append(r.order, bucket)
if r.adopted == nil {
r.adopted = map[string][]adoptedHear{}
}
subject := "mesh.seat." + seat + ".event." + event
fresh := len(r.adopted[subject]) == 0
r.adopted[subject] = append(r.adopted[subject], adoptedHear{bucket: bucket, hear: hear})
if !fresh {
subject = ""
}
return p, subject, 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 = f.put(kv)
} else {
err = f.drop(kv)
}
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 {
@@ -113,6 +306,9 @@ func (r *ProcessReader) Picture(bucket string) *Picture {
// as buildrun.FollowPlacement takes it); any other bucket is refused, for the reader reads its owners' states by their
// first views alone.
func (r *ProcessReader) KeyValue(_ context.Context, bucket string) (jetstream.KeyValue, error) {
if p := r.seatPicture(bucket); p != nil {
return p, nil
}
if p := r.Picture(bucket); p != nil {
return p, nil
}
@@ -145,12 +341,12 @@ func (r *ProcessReader) finding(s Status, raised bool) {
if r.Now != nil {
at = r.Now()
}
f := UnansweredFinding(r.Module, s, at)
f := unansweredFinding(r.Module, s, at)
var err error
if raised {
err = kv.Put(f)
err = f.put(kv)
} else {
err = kv.Delete(f)
err = f.drop(kv)
}
if err != nil {
r.logf("views: the finding of the first view %s is not written: %v", s.Tool, err)
@@ -159,26 +355,42 @@ func (r *ProcessReader) finding(s Status, raised bool) {
// Hear is the handler of the module's durable consumer on the shared events stream (cred.EventConsumer on
// process.EventsStream): an owner's views event (`mesh.mod.<owner>.event.views.<state>`) goes to the follower of that
// owner's state, any other message to Other.
// owner's state, any other message to Other. The followers are read at each message, so a view followed or adopted
// after Start hears its events and a dropped one no longer does (design 47 G §532).
func (r *ProcessReader) Hear(ctx context.Context) func(jetstream.Msg) {
fs := map[string]map[string]*Follower{}
for _, f := range r.Followers() {
if fs[f.Owner] == nil {
fs[f.Owner] = map[string]*Follower{}
}
fs[f.Owner][f.State] = f
}
views := map[string]func(jetstream.Msg){}
for owner, m := range fs {
views[owner] = Hear(ctx, m, r.logf)
}
return func(m jetstream.Msg) {
if owner, ok := OwnerOfEvent(m.Subject()); ok {
if h := views[owner]; h != nil {
h(m)
return
r.mu.Lock()
hears := adoptedHears(r.adopted, m.Subject())
var owned map[string]*Follower
if owner, ok := OwnerOfEvent(m.Subject()); ok && hears == nil {
for _, b := range r.order {
if f := r.pics[b].f; f.Owner == owner {
if owned == nil {
owned = map[string]*Follower{}
}
owned[f.State] = f
}
}
}
r.mu.Unlock()
if hears != nil {
failed := false
for _, h := range hears {
if err := h.hear(ctx, m.Data()); err != nil {
r.logf("views: %s is not taken: %v", m.Subject(), err)
// an event adopted with no picture is asked again by the bus (design 47 G A-222)
failed = failed || h.bucket == ""
}
}
if !failed {
_ = m.Ack()
}
return
}
if owned != nil {
Hear(ctx, owned, r.logf)(m)
return
}
if r.Other != nil {
r.Other(m)
return
@@ -201,18 +413,100 @@ func (r *ProcessReader) Owners() []string {
}
// Ask asks every first view until answered, each on a goroutine of its own, so the process starts at once and waits for
// an owner that is down, saying so (ADR 0615 §6). Said, when set, keeps each status (KeepStatus).
// an owner that is down, saying so (ADR 0615 §6). Said, when set, keeps each status (KeepStatus). A view followed or
// adopted after it is asked at once (design 47 G §532).
func (r *ProcessReader) Ask(ctx context.Context, said func(Status)) {
for _, f := range r.Followers() {
if said != nil {
f.Said = said
}
go func() {
if err := f.Start(ctx); err != nil {
r.logf("the first view %s was not answered: %v", f.Tool, err)
}
}()
r.mu.Lock()
r.ctx, r.said, r.asked = ctx, said, true
buckets := slices.Clone(r.order)
seats := slices.Collect(maps.Values(r.seats))
r.mu.Unlock()
for _, b := range buckets {
r.begin(b)
}
for _, sw := range seats {
r.runSeat(sw)
}
}
// begin asks bucket's first view once Ask ran, on a goroutine of its own, until answered or the view is dropped.
func (r *ProcessReader) begin(bucket string) {
r.mu.Lock()
p := r.pics[bucket]
if !r.asked || p == nil || r.begun[bucket] != nil {
r.mu.Unlock()
return
}
if r.begun == nil {
r.begun = map[string]context.CancelFunc{}
}
ctx, cancel := context.WithCancel(r.ctx)
r.begun[bucket] = cancel
f, said, created := p.f, r.said, r.created
r.mu.Unlock()
if said != nil {
f.Said = func(s Status) {
if r.holds(p) {
said(s)
}
}
}
if !created.IsZero() {
f.ConsumerMade(ctx, created)
}
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)
}
}()
}
// Drop ends the view of owner's 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 status leaves the state `first-views` and so 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 *ProcessReader) Drop(owner, state string) error {
bucket := owner + "_" + state
r.mu.Lock()
p := r.pics[bucket]
if p == nil {
r.mu.Unlock()
return fmt.Errorf("%s does not follow %s's %s", r.Module, owner, state)
}
cancel := r.begun[bucket]
delete(r.pics, bucket)
delete(r.begun, bucket)
r.order = slices.DeleteFunc(r.order, func(b string) bool { return b == bucket })
for subject, hs := range r.adopted {
if hs = slices.DeleteFunc(hs, func(h adoptedHear) bool { return h.bucket == bucket }); len(hs) == 0 {
delete(r.adopted, subject)
} else {
r.adopted[subject] = hs
}
}
status := r.status
r.mu.Unlock()
if cancel != nil {
cancel()
}
r.logf("views: %s drops its view %s.%s", r.Module, owner, state)
if status != nil {
if kv := status.KV(); kv != nil {
if err := kv.Delete(context.Background(), StatusKey(owner, state)); err != nil {
r.logf("views: the status of the dropped view %s.%s is not removed from %s: %v", owner, state, StatusState, err)
}
}
}
s := p.f.Status()
if s.Raised {
r.finding(s, false)
r.logf("views: %s clears its finding %s of the dropped view %s.%s", r.Module, UnansweredKind, owner, state)
}
if s.WaitsRaised {
r.waits(s, false)
r.logf("views: %s clears its finding %s of the dropped view %s.%s", r.Module, ViewWaitsKind, owner, state)
}
return nil
}
// Ready is closed once every followed state had its first view.
@@ -546,11 +840,33 @@ func (r *ProcessReader) Start(ctx context.Context, js jetstream.JetStream, cred
hub, pattern, own = process.NewEventHub(r.logf), ">", true
}
hub.OnBound(func(created time.Time) {
r.mu.Lock()
r.created = created
r.mu.Unlock()
for _, f := range r.Followers() {
f.ConsumerMade(ctx, created)
}
})
err := hub.Handle(pattern, r.Hear(ctx))
hear := r.Hear(ctx)
err := hub.Handle(pattern, hear)
r.mu.Lock()
r.status = status
if !own {
// an event adopted after the start is handled on the module's hub (Adopt); the reader's own hub takes every one
r.hub, r.handle = hub, hear
}
r.mu.Unlock()
if !own {
r.mu.Lock()
subjects := slices.Sorted(maps.Keys(r.adopted))
r.mu.Unlock()
for _, subject := range subjects {
// one subscription per event, with every further part (design 47 G §532)
if err == nil {
err = hub.Handle(subject+".>", hear)
}
}
}
if err == nil && own {
err = hub.Start(ctx, js, cred)
}
@@ -561,7 +877,8 @@ func (r *ProcessReader) Start(ctx context.Context, js jetstream.JetStream, cred
var said func(Status)
if status != nil {
status.Ready(func(kv jetstream.KeyValue) {
if err := HearAgain(ctx, kv, r.Followers(), r.logf); err != nil {
KeepOwnBuild(ctx, kv, process.OwnBuild(), r.logf)
if err := hearAgain(ctx, kv, r.Followers, r.logf); err != nil {
r.logf("views: the hand asks of %s's first views are not heard (%v) until it starts again", r.Module, err)
}
})
@@ -573,3 +890,18 @@ func (r *ProcessReader) Start(ctx context.Context, js jetstream.JetStream, cred
}
r.Ask(ctx, said)
}
// adoptedHears are the handlers of every follower adopted on the event whose subject is subject or has further parts
// after it (`<event>.<module>.<branch>.<commit or group>`, design 47 G §521, §532), or nil.
func adoptedHears(adopted map[string][]adoptedHear, subject string) []adoptedHear {
if h := adopted[subject]; h != nil {
return h
}
for i := strings.LastIndex(subject, "."); i > 0; i = strings.LastIndex(subject, ".") {
subject = subject[:i]
if h := adopted[subject]; h != nil {
return h
}
}
return nil
}