Files
mesh-sdk/go/process/seatstream.go
T
jschoubben 5fa64be894 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
2026-11-13 12:49:56 +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
}