From bb8b3bb43bc8e49fdea444ca94ce8711b67cd2ef Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 22 Oct 2026 17:34:35 +0200 Subject: [PATCH] =?UTF-8?q?Define=20how=20a=20module=20announces=20the=20s?= =?UTF-8?q?tates=20it=20keeps,=20so=20readers=20are=20granted=20from=20the?= =?UTF-8?q?=20owner's=20word=20(issue=201790,=20S9,=20ADR=200495=20=C2=A72?= =?UTF-8?q?-=C2=A73)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit announce.States on the module's own event subject mesh.mod..event.states, kept by the controller in mesh-controller_states-announced by module. --- go/announce/announce.go | 126 +++++++++++++++++++++++++++++++++++ go/announce/announce_test.go | 71 ++++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 go/announce/announce.go create mode 100644 go/announce/announce_test.go diff --git a/go/announce/announce.go b/go/announce/announce.go new file mode 100644 index 0000000..37dfd21 --- /dev/null +++ b/go/announce/announce.go @@ -0,0 +1,126 @@ +// Package announce is how a module says on the bus which states it keeps and which of them others may read (novox/hq +// ADR 0495 clauses 2 and 3; issue 1790): the subject it says it on, the value it says, and where the controller keeps +// the last one said by each module. What a module announced is the truth of what it keeps; a reader is granted a read +// from it, never from a list or from the owner's manifest. +// +// A module announces from its code (process.Conn.Keeps, stdio.Keeps, the TypeScript SDK's keeps), never from its +// manifest, which only the core reads (clause 1). A module that keeps no state on the bus announces nothing. +package announce + +import ( + "encoding/json" + "fmt" + "regexp" + "sort" +) + +// Version is the version of the value this package writes. +const Version = 1 + +// Event is the event type of an announcement, under the module's own namespace. +const Event = "states" + +// Subject is where module announces what it keeps: its own event subject, mesh.mod..event.states, which its +// account may already publish to and the events stream keeps. +func Subject(module string) string { return "mesh.mod." + module + ".event." + Event } + +// SubjectAll is every module's announcement, what the controller consumes. +const SubjectAll = "mesh.mod.*.event." + Event + +// KeptBucket is the controller's bucket of the last announcement of each module, by Key. +const KeptBucket = "mesh-controller_states-announced" + +var name = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`) + +// Key is module's key in KeptBucket: the module itself, which must be one plain name, since a module's name is part of +// its buckets' names (catalogue.StateProblems). +func Key(module string) (string, error) { + if !name.MatchString(module) { + return "", fmt.Errorf("announce: %q is not a module that keeps state: lower-case letters, digits and hyphens", module) + } + return module, nil +} + +// State is one state a module keeps, by its local name, and whether others may read it. Others never write it: a +// state another module writes is the owner's own code's business. +type State struct { + Name string `json:"name"` + Readable bool `json:"readable,omitempty"` +} + +// States is an announcement: the module, the machine whose instance said it, and every state the module keeps, sorted +// by name. An empty list says the module keeps nothing. +type States struct { + V int `json:"v"` + Module string `json:"module"` + Node string `json:"node,omitempty"` + States []State `json:"states"` +} + +// Keeps says whether the module announced keeping the state. +func (s States) Keeps(state string) bool { + for _, k := range s.States { + if k.Name == state { + return true + } + } + return false +} + +// Readable says whether the module announced the state readable by others. +func (s States) Readable(state string) bool { + for _, k := range s.States { + if k.Name == state { + return k.Readable + } + } + return false +} + +func (s *States) check() error { + if s.Module == "" { + return fmt.Errorf("announce: an announcement names no module") + } + if _, err := Key(s.Module); err != nil { + return err + } + seen := map[string]bool{} + for _, k := range s.States { + if !name.MatchString(k.Name) { + return fmt.Errorf("announce: %s announces the state %q, and a state is named locally: lower-case letters, digits and hyphens", s.Module, k.Name) + } + if seen[k.Name] { + return fmt.Errorf("announce: %s announces the state %q twice", s.Module, k.Name) + } + seen[k.Name] = true + } + return nil +} + +// Encode is s as JSON at Version, its states sorted; it refuses a module or a state no bucket could be named by and a +// state announced twice, naming it. +func Encode(s States) ([]byte, error) { + if err := s.check(); err != nil { + return nil, err + } + s.V = Version + s.States = append([]State{}, s.States...) + sort.Slice(s.States, func(i, j int) bool { return s.States[i].Name < s.States[j].Name }) + return json.Marshal(s) +} + +// Decode reads an announcement; it refuses another version and what Encode refuses. +func Decode(data []byte) (States, error) { + var s States + if err := json.Unmarshal(data, &s); err != nil { + return States{}, fmt.Errorf("announce: the announcement is not JSON: %w", err) + } + if s.V != Version { + return States{}, fmt.Errorf("announce: the announcement is version %d, and this reads version %d", s.V, Version) + } + if err := s.check(); err != nil { + return States{}, err + } + sort.Slice(s.States, func(i, j int) bool { return s.States[i].Name < s.States[j].Name }) + return s, nil +} diff --git a/go/announce/announce_test.go b/go/announce/announce_test.go new file mode 100644 index 0000000..63bc795 --- /dev/null +++ b/go/announce/announce_test.go @@ -0,0 +1,71 @@ +package announce + +import ( + "strings" + "testing" +) + +// A module says on the bus which states it keeps and which of them others may read (novox/hq ADR 0495 clause 2), on its +// own event subject, in a versioned value the controller keeps by module. +func TestAnAnnouncementRoundTripsSortedAndSaysWhatIsReadable(t *testing.T) { + data, err := Encode(States{Module: "mesh-controller", Node: "novox", States: []State{ + {Name: "plans-building", Readable: true}, {Name: "assignments", Readable: true}, {Name: "own"}}}) + if err != nil { + t.Fatal(err) + } + back, err := Decode(data) + if err != nil { + t.Fatal(err) + } + if back.V != Version || back.Module != "mesh-controller" || back.Node != "novox" { + t.Fatalf("decoded %+v", back) + } + var names []string + for _, s := range back.States { + names = append(names, s.Name) + } + if strings.Join(names, ",") != "assignments,own,plans-building" { + t.Fatalf("the states are not sorted: %v", names) + } + if !back.Readable("plans-building") || back.Readable("own") || back.Readable("absent") { + t.Fatal("Readable does not say what was announced readable") + } + if !back.Keeps("own") || back.Keeps("absent") { + t.Fatal("Keeps does not say what was announced") + } + if Subject("mesh-controller") != "mesh.mod.mesh-controller.event.states" || SubjectAll != "mesh.mod.*.event.states" { + t.Fatalf("the subjects are %q and %q", Subject("mesh-controller"), SubjectAll) + } + if KeptBucket != "mesh-controller_states-announced" { + t.Fatalf("the kept bucket is %q", KeptBucket) + } + if k, err := Key("mesh-delivery-builder"); err != nil || k != "mesh-delivery-builder" { + t.Fatalf("Key = %q, %v", k, err) + } + if nothing, err := Encode(States{Module: "quiet"}); err != nil || !strings.Contains(string(nothing), `"states":[]`) { + t.Fatalf("a module keeping nothing: %s, %v", nothing, err) + } +} + +func TestAnAnnouncementRefusesWhatNoBucketCouldBe(t *testing.T) { + for _, c := range []struct { + s States + says string + }{ + {States{Module: ""}, "no module"}, + {States{Module: "a.b"}, `"a.b"`}, + {States{Module: "a", States: []State{{Name: "x_y"}}}, `"x_y"`}, + {States{Module: "a", States: []State{{Name: ""}}}, `""`}, + {States{Module: "a", States: []State{{Name: "x"}, {Name: "x", Readable: true}}}, "twice"}, + } { + if _, err := Encode(c.s); err == nil || !strings.Contains(err.Error(), c.says) { + t.Errorf("%+v: %v, wanted a refusal saying %s", c.s, err, c.says) + } + } + if _, err := Decode([]byte(`{"v":2,"module":"a","states":[]}`)); err == nil { + t.Error("an unknown version was read") + } + if _, err := Key("a.b"); err == nil { + t.Error("a module with a dot became a key") + } +}