From 97e990c8f8df93c0d0aed84c1bec37f923cbe23b Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 21:37:12 +0100 Subject: [PATCH] =?UTF-8?q?=C2=A7556:=20a=20reader=20of=20a=20stream=20kep?= =?UTF-8?q?t=20last=20per=20subject=20is=20granted=20its=20ordered=20consu?= =?UTF-8?q?mer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds: design 47 G §556 (G-20 A-4) Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- .../internal/buscompose/modstreams.go | 8 +++++++ .../internal/buscompose/orderedread_test.go | 23 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 modules/mesh-grants/internal/buscompose/orderedread_test.go diff --git a/modules/mesh-grants/internal/buscompose/modstreams.go b/modules/mesh-grants/internal/buscompose/modstreams.go index 64e0cec1..af945c1b 100644 --- a/modules/mesh-grants/internal/buscompose/modstreams.go +++ b/modules/mesh-grants/internal/buscompose/modstreams.go @@ -260,6 +260,14 @@ 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 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). + 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+".*", + "$JS.API.CONSUMER.DELETE."+h.Stream+".*") + } } return out } diff --git a/modules/mesh-grants/internal/buscompose/orderedread_test.go b/modules/mesh-grants/internal/buscompose/orderedread_test.go new file mode 100644 index 00000000..fc61c48f --- /dev/null +++ b/modules/mesh-grants/internal/buscompose/orderedread_test.go @@ -0,0 +1,23 @@ +package buscompose + +import ( + "slices" + "testing" +) + +// A reader of a stream kept last per subject is granted what an ordered consumer of that stream uses, and a reader of +// another stream only its own durable (design 47 G §556, A-4). +func TestAReaderOfAStreamKeptLastPerSubjectIsGrantedItsOrderedConsumer(t *testing.T) { + p := Principal{Module: "m", Node: "a"} + ordered := []string{"$JS.API.CONSUMER.CREATE.S", "$JS.API.CONSUMER.MSG.NEXT.S.*", "$JS.API.CONSUMER.INFO.S.*", "$JS.API.CONSUMER.DELETE.S.*"} + last := heardGrants(p, []Heard{{Stream: "S", Filters: []string{"f"}, LastPerSubject: true}}) + for _, w := range ordered { + if !slices.Contains(last, w) { + t.Errorf("a last-per-subject reader is not granted %s: %v", w, last) + } + } + plain := heardGrants(p, []Heard{{Stream: "S", Filters: []string{"f"}}}) + if slices.Contains(plain, ordered[0]) { + t.Errorf("a reader of another stream is granted an ordered consumer: %v", plain) + } +}