diff --git a/go/catalogue/busnames.go b/go/catalogue/busnames.go new file mode 100644 index 00000000..fc1cf052 --- /dev/null +++ b/go/catalogue/busnames.go @@ -0,0 +1,88 @@ +package catalogue + +import ( + "regexp" + "slices" + "sort" + "strings" +) + +// The names of what a module keeps on the bus, as one rule (design 25 § 12.1; novox/hq ADR 0666 §16): the bus module +// creates them from a module's entries and the grants part grants them, and both take the names from here. + +// BusNameSeparator joins a module's name and an entry's local name into the name the mesh gives the entry on the bus. +const BusNameSeparator = "_" + +// SentObjectsLocal is the local name of a module's store of sent objects (the bodies of a reply or an event over the +// per-message limit): the bus module plans `_sent-objects` for every module, and no entry may take the name. +const SentObjectsLocal = "sent-objects" + +// ObjectsSuffix ends the local name of the object store beside a state: `-objects`. +const ObjectsSuffix = "-objects" + +// safeToken is a name that is one plain subject token. +var safeToken = regexp.MustCompile(`^[A-Za-z0-9_-]+$`) + +// BusName is `_`, the name the mesh gives an entry on the bus. +func BusName(module, local string) string { return module + BusNameSeparator + local } + +// StateObjectsName is the local name of the object store that holds the large values of the state `state`. +func StateObjectsName(state string) string { return state + ObjectsSuffix } + +// SentObjectsName is the bus name of a module's store of sent objects. +func SentObjectsName(module string) string { return BusName(module, SentObjectsLocal) } + +// BusStateStreams are the streams one bus-state entry of module is kept in on the bus, by the mesh's names +// `_`: a stream's own; a key-value bucket's KV_ stream with the OBJ_ stream of its object store +// `-objects`; an object store's OBJ_ stream; a ledger's history stream, its snapshot stream, and its state and +// index buckets' KV_ streams. A name that is not one plain subject token names nothing. +func BusStateStreams(module string, e BusStateEntry) []string { + base := BusName(module, e.Name) + if !safeToken.MatchString(base) || strings.Contains(e.Name, BusNameSeparator) || strings.Contains(module, BusNameSeparator) { + return nil + } + var out []string + switch e.Storage { + case StorageKeyValue: + out = []string{"KV_" + base} + if e.Template == TemplateState { + out = append(out, "OBJ_"+BusName(module, StateObjectsName(e.Name))) + } + case StorageObjectStore: + out = []string{"OBJ_" + base} + case StorageLedger: + out = []string{base, base + "_snapshot", "KV_" + base + "_state"} + for _, t := range e.Types { + for _, ix := range t.Indexes { + if safeToken.MatchString(ix.Name) { + out = append(out, "KV_"+base+"_index-"+ix.Name) + } + } + } + default: + out = []string{base} + } + sort.Strings(out) + return slices.Compact(out) +} + +// ObjectNameClashes are the local names of a module's bus-state entries that meet a name the mesh makes itself: the +// name of a state's object store (`-objects` beside the state ``) and the store of sent objects. The +// indexer's judge refuses each. +func ObjectNameClashes(entries []BusStateEntry) []string { + states := map[string]bool{} + for _, e := range entries { + if e.Storage == StorageKeyValue && e.Template == TemplateState { + states[e.Name] = true + } + } + var out []string + for _, e := range entries { + stem, ok := strings.CutSuffix(e.Name, ObjectsSuffix) + if e.Name == SentObjectsLocal || (ok && states[stem]) { + out = append(out, e.Name) + } + } + sort.Strings(out) + return slices.Compact(out) +} diff --git a/go/catalogue/busnames_test.go b/go/catalogue/busnames_test.go new file mode 100644 index 00000000..087b6484 --- /dev/null +++ b/go/catalogue/busnames_test.go @@ -0,0 +1,38 @@ +package catalogue + +import ( + "slices" + "testing" +) + +// ADR 0666 §16: the bus names of an entry are one rule. A state entry answers its bucket's stream and the stream of +// its object store; a local name that meets a name the mesh makes is a clash for the judge. +func TestTheBusNamesOfAnEntryAreOneRule(t *testing.T) { + state := BusStateEntry{Name: "views", Storage: StorageKeyValue, Template: TemplateState} + rows := []struct { + name string + entry BusStateEntry + streams []string + }{ + {"a state: its bucket and its object store", state, []string{"KV_m_views", "OBJ_m_views-objects"}}, + {"a key-value that is not a state template: its bucket only", BusStateEntry{Name: "x", Storage: StorageKeyValue}, []string{"KV_m_x"}}, + {"an object store", BusStateEntry{Name: "blobs", Storage: StorageObjectStore, Template: TemplateObjects}, []string{"OBJ_m_blobs"}}, + {"a stream", BusStateEntry{Name: "log", Storage: StorageStream, Template: TemplateLog}, []string{"m_log"}}, + {"a ledger", BusStateEntry{Name: "l", Storage: StorageLedger, Template: TemplateLedger}, []string{"KV_m_l_state", "m_l", "m_l_snapshot"}}, + {"a name that is not one token", BusStateEntry{Name: "a.b", Storage: StorageStream}, nil}, + {"a local name holding the separator", BusStateEntry{Name: "a_b", Storage: StorageStream}, nil}, + } + for _, r := range rows { + if got := BusStateStreams("m", r.entry); !slices.Equal(got, r.streams) { + t.Errorf("%s: streams %v, want %v", r.name, got, r.streams) + } + } + if got := SentObjectsName("m"); got != "m_sent-objects" { + t.Errorf("sent store %q", got) + } + clash := []BusStateEntry{state, {Name: "views-objects", Storage: StorageObjectStore}, {Name: "other-objects", Storage: StorageObjectStore}, + {Name: SentObjectsLocal, Storage: StorageObjectStore}} + if got := ObjectNameClashes(clash); !slices.Equal(got, []string{"sent-objects", "views-objects"}) { + t.Errorf("clashes %v", got) + } +}