The subscription's key resolves through mesh-sdk's buscompose.EventSubjects (lead answer A-153; the sdk commit EE-S1 is cherry-picked into this change's sdk placement), a wildcard is refused. Builds: design 47 G §559, §488, §576, §622 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
88 lines
2.6 KiB
Go
88 lines
2.6 KiB
Go
package bus
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
)
|
|
|
|
// SubscribeNew hears one event, named `<emitter>.<event>`, from the next message on, on an ordered consumer of its own on each
|
|
// stream that carries it, and hands every message to deliver as the envelope a module sees. The subjects the name lands on are
|
|
// mesh-sdk's one rule (buscompose.EventSubjects, design 47 G §559, A-153), so a name with a wildcard is refused, naming the rule;
|
|
// a name no stream on the bus carries is an error, never a subscription that hears nothing. stop ends every consumer.
|
|
func (c *Conn) SubscribeNew(name string, deliver func(json.RawMessage) error) (func(), error) {
|
|
subjects, err := buscompose.EventSubjects(name, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
js, err := c.JetStream()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), StateTimeout)
|
|
defer cancel()
|
|
// The streams that carry each subject, found by the bus: a namespace no stream carries hears nothing and is skipped.
|
|
byStream := map[string][]string{}
|
|
for _, subject := range subjects {
|
|
if strings.HasSuffix(subject, ".>") {
|
|
continue
|
|
}
|
|
stream, err := js.StreamNameBySubject(ctx, subject)
|
|
if err != nil || stream == "" {
|
|
continue
|
|
}
|
|
byStream[stream] = append(byStream[stream], subject, subject+".>")
|
|
}
|
|
if len(byStream) == 0 {
|
|
return nil, fmt.Errorf("no stream on the bus carries %s (%s): nothing would be heard", name, strings.Join(subjects, ", "))
|
|
}
|
|
var stops []func()
|
|
stopAll := func() {
|
|
for _, s := range stops {
|
|
s()
|
|
}
|
|
}
|
|
for _, stream := range slices.Sorted(mapKeys(byStream)) {
|
|
cons, err := js.OrderedConsumer(ctx, stream, jetstream.OrderedConsumerConfig{FilterSubjects: byStream[stream],
|
|
DeliverPolicy: jetstream.DeliverNewPolicy})
|
|
if err != nil {
|
|
stopAll()
|
|
return nil, fmt.Errorf("an ordered consumer of %s on %s: %w", name, stream, err)
|
|
}
|
|
cc, err := cons.Consume(func(m jetstream.Msg) {
|
|
env, ok := envelopeOf(m.Subject(), m.Headers(), m.Data())
|
|
if !ok {
|
|
return
|
|
}
|
|
raw, err := json.Marshal(env)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if err := deliver(raw); err != nil && c.Logf != nil {
|
|
c.Logf("[mesh-tools] a subscription for new %s messages did not take one: %v", name, err)
|
|
}
|
|
})
|
|
if err != nil {
|
|
stopAll()
|
|
return nil, fmt.Errorf("consuming %s on %s: %w", name, stream, err)
|
|
}
|
|
stops = append(stops, cc.Stop)
|
|
}
|
|
return stopAll, nil
|
|
}
|
|
|
|
func mapKeys(m map[string][]string) func(func(string) bool) {
|
|
return func(yield func(string) bool) {
|
|
for k := range m {
|
|
if !yield(k) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|