S1 sdk-stream-last-per-subject: a stream may declare last-per-subject

Builds: design 47 G §703, §50 (A-343, A-344)

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-13 11:36:07 +01:00
parent 27da949140
commit 4e21bae65a
2 changed files with 19 additions and 1 deletions
+4 -1
View File
@@ -64,6 +64,9 @@ type StreamDeclaration struct {
// Duplicates is a work-queue stream's duplicate window in hours, at most its maximum age; refused
// without retention work-queue (novox/hq ADR 0436 §6).
Duplicates int `json:"duplicates,omitempty"`
// LastPerSubject says the stream keeps only the last message of each subject, as a seat event's `keep` does (design 47 G §703,
// §396, A-181): a part that follows it reads it whole at each start (§556).
LastPerSubject bool `json:"last-per-subject,omitempty"`
// KeptLayout is the layout its holder says it writes: its form and its catch-up tool (design 47).
KeptLayout
}
@@ -86,7 +89,7 @@ func (s *StreamDeclaration) UnmarshalJSON(raw []byte) error {
dec.DisallowUnknownFields()
if err := dec.Decode(&p); err != nil {
return fmt.Errorf("a stream is {name, events, max-age-days, max-mib, grant, retention, max-deliveries, "+
"duplicates, form, catch-up}: %w", typedUnknown(err))
"duplicates, last-per-subject, form, catch-up}: %w", typedUnknown(err))
}
if err := p.KeptLayout.refuse(fmt.Sprintf("stream %q", p.Name)); err != nil {
return err
+15
View File
@@ -152,3 +152,18 @@ func TestNarrowingTwoPatterns(t *testing.T) {
t.Fatalf("uncovered: %v", got)
}
}
// A stream declares that it keeps the last message of each subject (design 47 G §703), says no unless it does, and a stream
// without it reads as before.
func TestAStreamDeclaresItIsKeptLastPerSubject(t *testing.T) {
m, err := decodeForTest([]byte(`{"module":"feed","version":"1","streams":[{"name":"ends","events":["end.**"],"last-per-subject":true},{"name":"all","events":["a.**"]}]}`))
if err != nil {
t.Fatal(err)
}
if !m.Streams[0].LastPerSubject || m.Streams[1].LastPerSubject || m.UnknownField() != "" {
t.Fatalf("last-per-subject read as %+v (set aside %q)", m.Streams, m.UnknownField())
}
if !slices.Contains(ManifestReads(), "word:streams.last-per-subject") {
t.Fatal("ManifestReads does not state word:streams.last-per-subject")
}
}