SD-M1 sdk: a seat's family is the shared events stream's, 7 days, no cap of its own
The lead's ruling of 13:0x replaces 12:5x's said-subject: mesh-broker's events follow every seat, on mesh.seat.mesh-broker.event.<event> in the shared events stream, 7 days; the bus module keeps a longer history on a stream of its own module. So every seat family is that stream filtered to the seat's subjects: a definition says 7 days and no cap of its own, and the reader refuses anything else. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
@@ -1,16 +1,10 @@
|
||||
package buscompose
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshbroker"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/operatorchannel"
|
||||
"git.novox.be/novox/mesh-sdk/go/membership"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
)
|
||||
|
||||
// The names this package keeps for the grants part's composer (mesh-grants internal/buscompose, ADR 0593 §1) agree with
|
||||
@@ -39,38 +33,10 @@ func TestTheBusNamesAgree(t *testing.T) {
|
||||
if err := ledger.Valid(); err != nil || ledger.OwnSecret() != "ledgers" {
|
||||
t.Fatalf("a wanted ledger account: %v, held in %q", err, ledger.OwnSecret())
|
||||
}
|
||||
// mesh-broker's events are the bus module's own stream (design 25 § 12.3, 30 days): no subject of the shared
|
||||
// streams may take them, or the bus refuses the two streams as overlapping.
|
||||
// A seat's family is the shared events stream filtered to its subjects: a definition says the stream's age.
|
||||
for _, st := range MeshStreams() {
|
||||
for _, pattern := range st.Subjects {
|
||||
if subjectTakes(pattern, meshbroker.SubjectCreated) {
|
||||
t.Errorf("the stream %s takes mesh-broker's %s by %s", st.Name, meshbroker.SubjectCreated, pattern)
|
||||
}
|
||||
}
|
||||
}
|
||||
// A seat family on the shared events stream is kept as long as the stream keeps it: its contract says no more.
|
||||
for family, days := range map[string]int{operatorchannel.Family: operatorchannel.FamilyMaxAgeDays,
|
||||
npmpackageregistry.Family: npmpackageregistry.FamilyMaxAgeDays, gopackageregistry.Family: gopackageregistry.FamilyMaxAgeDays,
|
||||
containerimageregistry.Family: containerimageregistry.FamilyMaxAgeDays,
|
||||
ociartifactregistry.Family: ociartifactregistry.FamilyMaxAgeDays} {
|
||||
for _, st := range MeshStreams() {
|
||||
if st.Name == EventsStream && int64(days)*24*60*60 != int64(st.MaxAge) {
|
||||
t.Errorf("the family %s says %d days, the shared events stream keeps %d s", family, days, st.MaxAge)
|
||||
}
|
||||
if st.Name == EventsStream && int64(st.MaxAge) != seatdef.SharedEventsMaxAgeDays*24*60*60 {
|
||||
t.Errorf("the shared events stream keeps %d s, a seat's definition says %d days", st.MaxAge, seatdef.SharedEventsMaxAgeDays)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// subjectTakes says whether a subject pattern (`*` one token, `>` the rest) takes a subject.
|
||||
func subjectTakes(pattern, subject string) bool {
|
||||
p, s := strings.Split(pattern, "."), strings.Split(subject, ".")
|
||||
for i, tok := range p {
|
||||
switch {
|
||||
case tok == ">":
|
||||
return len(s) > i
|
||||
case i >= len(s) || (tok != "*" && tok != s[i]):
|
||||
return false
|
||||
}
|
||||
}
|
||||
return len(p) == len(s)
|
||||
}
|
||||
|
||||
+11
-37
@@ -67,8 +67,7 @@ type Definition struct {
|
||||
Verbs []Verb `json:"verbs,omitempty"`
|
||||
// Accepts are what may be sent to the seat's holder on `mesh.seat.<seat>.accept.<name>`.
|
||||
Accepts []Message `json:"accepts,omitempty"`
|
||||
// Events are what is published on the seat's own subject, `mesh.seat.<seat>.<token>.<name>` (EventSubject): the
|
||||
// token is `event`, unless the family names its own.
|
||||
// Events are what is published on the seat's own subject, `mesh.seat.<seat>.event.<name>`.
|
||||
Events []Message `json:"events,omitempty"`
|
||||
// Family is where the seat's events are kept, when the definition fixes it.
|
||||
Family *Family `json:"family,omitempty"`
|
||||
@@ -118,24 +117,17 @@ type HolderEvent struct {
|
||||
Body map[string]any `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
// Family is where a seat's events are kept.
|
||||
// Family is where a seat's events are kept: the shared events stream filtered to the seat's subjects, so its age is
|
||||
// the stream's (SharedEventsMaxAgeDays) and it has no cap of its own (the lead's ruling of 2026-11-04 13:0x: a
|
||||
// holder that keeps a longer history keeps it in a stream of its own module).
|
||||
type Family struct {
|
||||
Name string `json:"name"`
|
||||
MaxAgeDays int `json:"max-age-days"`
|
||||
MaxMiB int `json:"max-mib,omitempty"`
|
||||
// Token is the subject's word after the seat's name, `event` when absent. A family kept in a stream of its own
|
||||
// names another, so its subjects stay outside the shared events stream's `mesh.seat.*.event.>`: the bus refuses
|
||||
// two streams whose subjects overlap (mesh-broker's `said`, design 25 § 12.3).
|
||||
Token string `json:"token,omitempty"`
|
||||
}
|
||||
|
||||
// EventToken is the token after the seat's name on the subjects of its events.
|
||||
func (f *Family) EventToken() string {
|
||||
if f == nil || f.Token == "" {
|
||||
return "event"
|
||||
}
|
||||
return f.Token
|
||||
}
|
||||
// SharedEventsMaxAgeDays is how long the shared events stream keeps a seat's events (mesh-sdk buscompose's
|
||||
// EventsStream, whose test holds the two equal).
|
||||
const SharedEventsMaxAgeDays = 7
|
||||
|
||||
// Record is one state the holder keeps for the seat: its key's form and its value's schema.
|
||||
type Record struct {
|
||||
@@ -387,16 +379,9 @@ func (d Definition) check() Problems {
|
||||
if d.Family.Name == "" {
|
||||
ps = append(ps, Problem{"family.name", "missing"})
|
||||
}
|
||||
if d.Family.MaxAgeDays < 1 {
|
||||
ps = append(ps, Problem{"family.max-age-days", "missing, or under one day"})
|
||||
}
|
||||
switch t := d.Family.Token; {
|
||||
case t == "":
|
||||
case !wordName.MatchString(t):
|
||||
ps = append(ps, Problem{"family.token", fmt.Sprintf("%q is not a lower-case word", t)})
|
||||
case sharedTokens[t]:
|
||||
ps = append(ps, Problem{"family.token", fmt.Sprintf("%q is a token every seat shares; "+
|
||||
"a family of its own names another, or none", t)})
|
||||
if d.Family.MaxAgeDays != SharedEventsMaxAgeDays {
|
||||
ps = append(ps, Problem{"family.max-age-days", fmt.Sprintf("missing, or not %d: a seat's events ride the "+
|
||||
"shared events stream, which keeps them %d days", SharedEventsMaxAgeDays, SharedEventsMaxAgeDays)})
|
||||
}
|
||||
}
|
||||
for i, e := range d.HolderEvents {
|
||||
@@ -517,20 +502,9 @@ func Resolve(d Definition, parts map[string]Part) (Definition, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// EventSubject is where the seat's event is published on the shared events stream's token; by a caller, the
|
||||
// caller's name follows. A definition whose family names a token of its own is asked with Definition.EventSubject.
|
||||
// EventSubject is where the seat's event is published; by a caller, the caller's name follows.
|
||||
func EventSubject(seat, event string) string { return "mesh.seat." + seat + ".event." + event }
|
||||
|
||||
// EventSubject is where this seat's event is published, under its family's token; by a caller, the caller's name
|
||||
// follows.
|
||||
func (d Definition) EventSubject(event string) string {
|
||||
return "mesh.seat." + d.Seat + "." + d.Family.EventToken() + "." + event
|
||||
}
|
||||
|
||||
// sharedTokens are the words after a seat's name that every seat's subjects share (its verbs, its work queue, the
|
||||
// shared events stream's events): a family's own token is none of them.
|
||||
var sharedTokens = map[string]bool{"event": true, "accept": true, "tool": true, "call": true, "proof": true}
|
||||
|
||||
// AcceptSubject is where a message the seat accepts is sent.
|
||||
func AcceptSubject(seat, accept string) string { return "mesh.seat." + seat + ".accept." + accept }
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ func TestADefinitionIsReadStrictlyNamingWhatIsWrong(t *testing.T) {
|
||||
"verbs":[{"name":"add","input":{"type":"object"}}]}`
|
||||
const whole = `{"format":1,"seat":"mesh-broker","scope":"mesh","grain":"whole","decision":"ADR 1",
|
||||
"verbs":[{"name":"list","input":{"type":"object"},"answer":{"type":"object"}}],
|
||||
"events":[{"name":"created","body":{"type":"object"}}],"family":{"name":"broker.events","max-age-days":30}}`
|
||||
"events":[{"name":"created","body":{"type":"object"}}],"family":{"name":"broker.events","max-age-days":7}}`
|
||||
cases := []struct {
|
||||
name, raw string
|
||||
refused string // a text the refusal names, empty when it parses
|
||||
@@ -35,13 +35,12 @@ func TestADefinitionIsReadStrictlyNamingWhatIsWrong(t *testing.T) {
|
||||
"verbs[0].input: missing"},
|
||||
{"a whole event with no body", strings.Replace(whole, `"name":"created","body":{"type":"object"}`,
|
||||
`"name":"created"`, 1), "events[0].body: missing"},
|
||||
{"whole events kept nowhere", strings.Replace(whole, `,"family":{"name":"broker.events","max-age-days":30}`,
|
||||
{"whole events kept nowhere", strings.Replace(whole, `,"family":{"name":"broker.events","max-age-days":7}`,
|
||||
"", 1), "family: missing"},
|
||||
{"a family's own token parses", strings.Replace(whole, `"max-age-days":30}`, `"max-age-days":30,"token":"said"}`, 1), ""},
|
||||
{"a family's token shared by every seat", strings.Replace(whole, `"max-age-days":30}`,
|
||||
`"max-age-days":30,"token":"event"}`, 1), "family.token: \"event\" is a token every seat shares"},
|
||||
{"a family's token not a word", strings.Replace(whole, `"max-age-days":30}`,
|
||||
`"max-age-days":30,"token":"sa.id"}`, 1), "family.token"},
|
||||
{"a family kept longer than the shared events stream", strings.Replace(whole, `"max-age-days":7`,
|
||||
`"max-age-days":30`, 1), "family.max-age-days: missing, or not 7"},
|
||||
{"a family with a cap of its own", strings.Replace(whole, `"max-age-days":7`, `"max-age-days":7,"max-mib":256`, 1),
|
||||
"max-mib: a field the format does not know"},
|
||||
{"a verb twice", strings.Replace(listGrain, `]}`, `,{"name":"add"}]}`, 1), `"add" twice`},
|
||||
{"records at the seat list's grain", strings.Replace(listGrain, `"decision":"ADR 1",`,
|
||||
`"decision":"ADR 1","records":[{"name":"asks"}],`, 1), "grain: seat-list holds"},
|
||||
@@ -94,18 +93,6 @@ func TestADefinitionIsReadStrictlyNamingWhatIsWrong(t *testing.T) {
|
||||
t.Errorf("a type defined twice: %v", err)
|
||||
}
|
||||
|
||||
// An event's subject takes its family's token: the shared events stream's `event`, or the family's own.
|
||||
for _, c := range []struct{ token, want string }{{"", "mesh.seat.mesh-broker.event.created"},
|
||||
{"said", "mesh.seat.mesh-broker.said.created"}} {
|
||||
d, err := Parse([]byte(strings.Replace(whole, `"max-age-days":30}`, `"max-age-days":30,"token":"`+c.token+`"}`, 1)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := d.EventSubject("created"); got != c.want {
|
||||
t.Errorf("token %q: subject %q, want %q", c.token, got, c.want)
|
||||
}
|
||||
}
|
||||
|
||||
// Fixtures name what they are an instance of.
|
||||
if _, err := ParseFixtures([]byte(`[{"name":"a","of":"verb:list.input","given":{}},{"name":"b","of":"event:created",
|
||||
"subject":"mesh.seat.mesh-broker.event.created","given":{}}]`)); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user