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..`, the // seat mesh-delivery's `owed`). Its name on the bus is seatdef.KeptStream's, `SEAT__`, which a generated // contract carries as Stream: 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: _, 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, 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 is %s_, "+ "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 }