// 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"` // Readers, when named, are the only modules that may read the state (novox/hq ADR 0547 clause 9, issue 1945): a // state readable by some and not all, such as the vault's bus account hashes. Only ReadableBy grants them; Readable, // which a composer that knows no readers asks, stays false for it, so such a composer grants nobody. Readers []string `json:"readers,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 } // ReadableBy says whether the module announced the state readable by reader: readable, and reader among the readers // when it names any. func (s States) ReadableBy(state, reader string) bool { for _, k := range s.States { if k.Name != state { continue } if len(k.Readers) > 0 { for _, r := range k.Readers { if r == reader { return true } } return false } return k.Readable } 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 for _, r := range k.Readers { if !name.MatchString(r) { return fmt.Errorf("announce: %s names %q as a reader of %s, and a reader is a module: lower-case letters, digits and hyphens", s.Module, r, k.Name) } } } 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 }