views: an owner's changes are ordinary events on the shared events stream, heard on the reader's one durable consumer; Follow takes no stream, Other takes the rest (ADR 0635 §5, D-25; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 14:36:41 +01:00
parent 129dc8cffb
commit 02fe0a015b
6 changed files with 178 additions and 68 deletions
+53 -40
View File
@@ -21,7 +21,6 @@ import (
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/process"
)
@@ -43,10 +42,14 @@ type ProcessReader struct {
Bound time.Duration
Wait time.Duration
mu sync.Mutex
pics map[string]*Picture // by bucket name <owner>_<state>
order []string
streams map[string]string // each owner's views stream, by owner
// Other is told of each message of the module's durable consumer on the shared events stream that is not an owner's
// views event, so a module that hears other events through the same consumer (a module has one) handles them there.
// Nil says them in the log, acknowledged: the module hears views events alone.
Other func(jetstream.Msg)
mu sync.Mutex
pics map[string]*Picture // by bucket name <owner>_<state>
order []string
}
func (r *ProcessReader) logf(format string, args ...any) {
@@ -55,23 +58,19 @@ func (r *ProcessReader) logf(format string, args ...any) {
}
}
// Follow adds the owner's state to follow, as the owner declares them (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), stream its views
// family as the owner's manifest's `streams` names it (its bus name is catalogue.StreamBusName's). views names no owner:
// the caller gives both. An owner has one views stream; a second name for it is refused. Call it before Start.
func (r *ProcessReader) Follow(owner, state, tool, stream string) (*Picture, error) {
// 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.
func (r *ProcessReader) Follow(owner, state, tool string) (*Picture, error) {
r.mu.Lock()
defer r.mu.Unlock()
if owner == "" || state == "" || !strings.HasPrefix(tool, owner+".") || stream == "" {
return nil, fmt.Errorf("%s follows %s's %s by its read tool <owner>.<tool> and its views stream, not %q and %q", r.Module, owner, state, tool, stream)
}
if have, ok := r.streams[owner]; ok && have != stream {
return nil, fmt.Errorf("%s's views stream is %s, not %s", owner, have, stream)
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)
}
if r.pics == nil {
r.pics, r.streams = map[string]*Picture{}, map[string]string{}
r.pics = map[string]*Picture{}
}
r.streams[owner] = stream
bucket := owner + "_" + state
if p := r.pics[bucket]; p != nil {
return p, nil
@@ -153,16 +152,35 @@ func (r *ProcessReader) finding(s Status, raised bool) {
}
}
// Hear is the handler of the owner's views family on the reader's durable consumer (cred.EventConsumer on the owner's
// views stream): each event to the follower of its state.
func (r *ProcessReader) Hear(ctx context.Context, owner string) func(jetstream.Msg) {
fs := map[string]*Follower{}
// 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.
func (r *ProcessReader) Hear(ctx context.Context) func(jetstream.Msg) {
fs := map[string]map[string]*Follower{}
for _, f := range r.Followers() {
if f.Owner == owner {
fs[f.State] = f
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
}
}
if r.Other != nil {
r.Other(m)
return
}
r.logf("views: %s is no views event of an owner followed, acknowledged", m.Subject())
_ = m.Ack()
}
return Hear(ctx, fs, r.logf)
}
// Owners are the owners followed, sorted.
@@ -510,23 +528,18 @@ func (e entry) Created() time.Time { return e.at }
func (e entry) Delta() uint64 { return 0 }
func (e entry) Operation() jetstream.KeyValueOp { return e.op }
// Start hears each owner's views family on the process's durable consumer of it (its manifest's `consumes`
// `<owner>.views.<state>`), keeps each status in the process's state `first-views` and hears the hand asks the tools
// bundle's `<reader>_first_view_again` writes there (StatusTools), then asks every first view (Ask). A consumer
// the bus does not have yet is said: the picture is then the first view's alone, asked again on a gap or by hand.
// Start hears the owners' views events on the process's durable consumer of the shared events stream (its manifest's
// `consumes` `<owner>.views.<state>`), keeps each status in the process's state `first-views` and hears the hand asks the
// tools bundle's `<reader>_first_view_again` writes there (StatusTools), then asks every first view (Ask). A consumer the
// bus does not have yet is said: the picture is then the first view's alone, asked again on a gap or by hand.
func (r *ProcessReader) Start(ctx context.Context, js jetstream.JetStream, cred process.Account, status *process.AwaitedBucket) {
for _, owner := range r.Owners() {
r.mu.Lock()
stream := catalogue.StreamBusName(r.streams[owner])
r.mu.Unlock()
cons, err := cred.EventConsumer(ctx, js, stream)
if err == nil {
_, err = process.HearEvents(cons, r.Hear(ctx, owner), r.logf)
}
if err != nil {
r.logf("the consumer %s on %s is not heard (%v): %s's states are the first view's alone, asked again by hand, until the %s starts again",
cred.EventConsumerName(), stream, err, owner, r.Module)
}
cons, err := cred.EventConsumer(ctx, js, process.EventsStream)
if err == nil {
_, err = process.HearEvents(cons, r.Hear(ctx), r.logf)
}
if err != nil {
r.logf("the consumer %s on %s is not heard (%v): the states followed are the first views' alone, asked again by hand, until the %s starts again",
cred.EventConsumerName(), process.EventsStream, err, r.Module)
}
var said func(Status)
if status != nil {