diff --git a/modules/mesh-grants/internal/buscompose/modstreams.go b/modules/mesh-grants/internal/buscompose/modstreams.go index 4b32fff5..d030a002 100644 --- a/modules/mesh-grants/internal/buscompose/modstreams.go +++ b/modules/mesh-grants/internal/buscompose/modstreams.go @@ -273,15 +273,18 @@ func heardGrants(p Principal, hears []Heard) []string { "$JS.API.CONSUMER.INFO."+h.Stream+"."+durable, "$JS.API.CONSUMER.MSG.NEXT."+h.Stream+"."+durable, "$JS.ACK."+h.Stream+"."+durable+".>") - if strings.HasPrefix(h.Stream, seatdef.KeptStreamPrefix) || h.LastPerSubject { + seatKept := strings.HasPrefix(h.Stream, seatdef.KeptStreamPrefix) + if seatKept || h.LastPerSubject { // A seat's kept event, and a module's own stream kept last per subject, are heard through process.HearSeatEvents and // process.HearStream, which reads the stream's own info to see whether it is // kept last per subject (design 47 G §641, A-19): the info of this stream alone. out = append(out, "$JS.API.STREAM.INFO."+h.Stream) } - if h.LastPerSubject { + if seatKept || h.LastPerSubject { // A stream kept last per subject is read whole at each start through an ordered consumer, made for that one read // and with no durable name: its creation, its next message, its info and the deletion of its own (design 47 G §556, A-4). + // So is every seat's kept event, whatever its keep: one kept by age is read whole from its first message with + // process.FromFirst, with no manifest word (A-233). out = append(out, "$JS.API.CONSUMER.CREATE."+h.Stream, "$JS.API.CONSUMER.CREATE."+h.Stream+".>", "$JS.API.CONSUMER.MSG.NEXT."+h.Stream+".*", "$JS.API.CONSUMER.INFO."+h.Stream+".*", diff --git a/modules/mesh-grants/internal/buscompose/orderedread_test.go b/modules/mesh-grants/internal/buscompose/orderedread_test.go index 96fb5fc9..642811f1 100644 --- a/modules/mesh-grants/internal/buscompose/orderedread_test.go +++ b/modules/mesh-grants/internal/buscompose/orderedread_test.go @@ -20,6 +20,14 @@ func TestAReaderOfAStreamKeptLastPerSubjectIsGrantedItsOrderedConsumer(t *testin if slices.Contains(plain, ordered[0]) { t.Errorf("a reader of another stream is granted an ordered consumer: %v", plain) } + // A seat's kept event kept by age is read whole from its first message (process.FromFirst, A-233): its ordered consumer too. + byAge := heardGrants(p, []Heard{{Stream: "SEAT_mesh-catalog_version-released", Filters: []string{"f"}}}) + for _, w := range []string{"$JS.API.CONSUMER.CREATE.SEAT_mesh-catalog_version-released", "$JS.API.CONSUMER.MSG.NEXT.SEAT_mesh-catalog_version-released.*", + "$JS.API.CONSUMER.INFO.SEAT_mesh-catalog_version-released.*", "$JS.API.CONSUMER.DELETE.SEAT_mesh-catalog_version-released.*"} { + if !slices.Contains(byAge, w) { + t.Errorf("a reader of a seat's event kept by age is not granted %s: %v", w, byAge) + } + } } // A reader of a seat's kept event is granted the info of that stream, which process.HearSeatEvents reads to see whether the