Files
mesh-sdk/go/process/seatstream.go
T
jschoubben 2134806396 A-233 (sdk): HearStream reads a stream kept by age whole from its first message with FromFirst
At every start an ordered consumer, made for the one read, hands every message from the first, then the
durable is followed, so a part that keeps what an age-kept seat event said (the assigner's version-released
and version-delivered) knows it again after a restart.

Builds: design 47 G A-233, A-189, A-181

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 06:19:55 +01:00

171 lines
7.9 KiB
Go

package process
import (
"context"
"fmt"
"strings"
"sync"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/seatdef"
)
// A seat's event kept in a stream of its own (seatdef's `keep`: the last message of each subject, or a stream with an age
// and a cap), not on the shared events stream (novox/hq design 47: the node-engine seat's `running.<node>.<module>`, the
// seat mesh-delivery's `owed`). Its name on the bus is seatdef.KeptStream's, `SEAT_<seat>_<event>`, which a generated
// contract carries as Stream<E>: one name in one place. These are the one rule, for the part that makes the stream's
// consumers (the grants part, from each module's `consumes`) and for every part that hears it, of the module's durable
// on it.
// keptStreamName is whether stream is a name seatdef.KeptStream gives: the bus's, with its prefix and no dot.
func keptStreamName(stream string) bool {
return strings.HasPrefix(stream, seatdef.KeptStreamPrefix) && len(stream) > len(seatdef.KeptStreamPrefix) &&
!strings.Contains(stream, ".")
}
// SeatEventConsumerConfig is the module's durable on a seat event's own stream as its maker makes it: <node>_<module>, as
// on the shared stream, filtered to the subjects the module consumes, acknowledged one by one; for an event kept last
// per subject it starts from the last message of each subject, never from now, so a consumer made after a subject was
// last said still hears it, and two parts' consumers made at different times hold the same (design 47).
func (a Account) SeatEventConsumerConfig(subjects []string, lastPerSubject bool) jetstream.ConsumerConfig {
cfg := jetstream.ConsumerConfig{Durable: a.EventConsumerName(), AckPolicy: jetstream.AckExplicitPolicy, FilterSubjects: subjects}
if lastPerSubject {
cfg.DeliverPolicy = jetstream.DeliverLastPerSubjectPolicy
}
return cfg
}
// What HearSeatEvents says of its start read through OnRead (design 47 G §641, A-19): `waiting` when it begins the read of
// a stream kept last per subject, `read` when the read has ended and the durable is followed.
const (
ReadWaiting = "waiting"
ReadDone = "read"
)
// HearOption is an option of HearSeatEvents.
type HearOption func(*hearOptions)
type hearOptions struct {
onRead func(state string)
fromFirst bool
}
// OnRead is the callback HearSeatEvents calls with ReadWaiting and then ReadDone around its start read; the module wires it
// to its first views (design 47 G A-19), so this package imports nothing of views.
func OnRead(f func(state string)) HearOption { return func(o *hearOptions) { o.onRead = f } }
// FromFirst is HearStream's form for a stream kept by age and cap, not last per subject (design 47 G A-233, A-189): at every
// start the stream is read whole from its first message through an ordered consumer made for that one read, on the subjects of
// the durable, and only then is the durable followed. A message the durable has not acknowledged yet is so handed twice, once
// by the read and once by the durable: the handler takes a message again as it took it.
func FromFirst() HearOption { return func(o *hearOptions) { o.fromFirst = true } }
// readWhole hands handle every message an ordered consumer, made on stream for the one read, delivers from the last of each
// subject (or, from DeliverAll, from the first message, FromFirst) filtered to subjects, until none is left pending (design 47 G §556): it ends when the message it delivered has
// none after it, or at once when the consumer, made, holds none.
func readWhole(ctx context.Context, js jetstream.JetStream, stream string, subjects []string, handle func(jetstream.Msg), from jetstream.DeliverPolicy) error {
cons, err := js.OrderedConsumer(ctx, stream, jetstream.OrderedConsumerConfig{FilterSubjects: subjects, DeliverPolicy: from})
if err != nil {
return err
}
last := make(chan struct{})
var once sync.Once
cc, err := cons.Consume(func(m jetstream.Msg) {
handle(m)
if md, err := m.Metadata(); err == nil && md.NumPending == 0 {
once.Do(func() { close(last) })
}
})
if err != nil {
return err
}
defer cc.Stop()
if info := cons.CachedInfo(); info != nil && info.NumPending == 0 && info.Delivered.Consumer == 0 {
return nil
}
select {
case <-last:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
// HearSeatEvents binds the module's durable on the stream (a contract's Stream<E>, as it stands) and pulls it, handing each
// message to handle, which acknowledges it; every error the bus reports while hearing is said through logf. Like
// EventHub it makes neither the stream nor the consumer: one the bus does not hold yet is this call's error, naming
// both, and a later call binds it. The caller stops the returned context.
//
// A stream the bus's own info says is kept last per subject is first read whole (design 47 G §641, §556, A-19): through an
// ordered consumer made for that one read, on the subjects of the durable, to the last of each, its messages going to the
// same handle; only then is the durable followed, so until the read ends the part hears none of it. OnRead says `waiting`
// before the read and `read` after it.
func (a Account) HearSeatEvents(ctx context.Context, js jetstream.JetStream, stream string, handle func(jetstream.Msg), logf func(string, ...any), opts ...HearOption) (jetstream.ConsumeContext, error) {
if !keptStreamName(stream) {
return nil, fmt.Errorf("%q is not the bus's name of a seat event's stream: a contract's Stream<E> is %s<seat>_<event>, "+
"with no dot (seatdef.KeptStream)", stream, seatdef.KeptStreamPrefix)
}
return a.HearStream(ctx, js, stream, handle, logf, opts...)
}
// wholeRead is how a start read begins: from the last of each subject for a stream kept last per subject, from the first
// message for FromFirst's, and none for another stream.
func wholeRead(maxPerSubject int64, fromFirst bool) (jetstream.DeliverPolicy, bool) {
switch {
case maxPerSubject == 1:
return jetstream.DeliverLastPerSubjectPolicy, true
case fromFirst:
return jetstream.DeliverAllPolicy, true
}
return 0, false
}
// HearStream is HearSeatEvents for the stream of a module's own events (a manifest's `streams` entry, named as
// catalogue.StreamBusName names it), which the module is granted through its `consumes`: the same one rule, a stream the bus's own
// info says is kept last per subject (`last-per-subject`) read whole first, then the durable followed (design 47 G §556, §703, A-181);
// with FromFirst, a stream kept by age is read whole from its first message the same way (A-233).
func (a Account) HearStream(ctx context.Context, js jetstream.JetStream, stream string, handle func(jetstream.Msg), logf func(string, ...any), opts ...HearOption) (jetstream.ConsumeContext, error) {
if handle == nil {
return nil, fmt.Errorf("hearing %s needs a handler", stream)
}
if logf == nil {
logf = func(string, ...any) {}
}
var o hearOptions
for _, f := range opts {
f(&o)
}
cons, err := a.EventConsumer(ctx, js, stream)
if err != nil {
return nil, fmt.Errorf("the consumer %s on %s is not there: %w", a.EventConsumerName(), stream, err)
}
st, err := js.Stream(ctx, stream)
if err != nil {
return nil, fmt.Errorf("the stream %s is not read: %w", stream, err)
}
if from, whole := wholeRead(st.CachedInfo().Config.MaxMsgsPerSubject, o.fromFirst); whole {
if o.onRead != nil {
o.onRead(ReadWaiting)
}
cfg := cons.CachedInfo().Config
subjects := cfg.FilterSubjects
if cfg.FilterSubject != "" {
subjects = append(subjects, cfg.FilterSubject)
}
if err := readWhole(ctx, js, stream, subjects, handle, from); err != nil {
return nil, fmt.Errorf("the start read of %s is not made: %w", stream, err)
}
if o.onRead != nil {
o.onRead(ReadDone)
}
logf("events: %s is read whole at its start", stream)
}
cc, err := HearEvents(cons, handle, logf)
if err != nil {
return nil, fmt.Errorf("the consumer %s on %s is not heard: %w", a.EventConsumerName(), stream, err)
}
logf("events: the consumer %s on %s is heard", a.EventConsumerName(), stream)
return cc, nil
}