Files
mesh-sdk/go/announce/announce.go
T
jschoubben 175a5567b8 The bus node follows the grants part's sealed user list, and only the grants part reads the vault's hashes (ADR 0547 clauses 7, 9; issue 1945)
The grants part writes its list as an object of its own bucket, sealed to the bus node's key, and names it in
user-list.sealed; the node-engine of the node holding mesh-broker is granted that one key and an object by name, the
grants part its bucket (the controller's copy carries the same grants, byte for byte, golden regenerated there).
A state may name its readers (announce, NamedReaders): mesh-vault.bus-accounts is read by mesh-grants alone.
2026-10-26 11:53:11 +01:00

155 lines
5.3 KiB
Go

// 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.<module>.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. Naming readers makes it readable.
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 || len(k.Readers) > 0
}
}
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
}