130 lines
6.0 KiB
Go
130 lines
6.0 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// stateName is one local name of a module's state: lower-case, no dot, no underscore.
|
|
var stateName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`)
|
|
|
|
// The mesh's caps on what a module may ask of a bucket's history.
|
|
const (
|
|
// StateMostHistory is the most past values a key may keep. The server's own limit.
|
|
StateMostHistory = 64
|
|
)
|
|
|
|
// StateDeclaration is one bucket a module owns: its local name, and the options that are the
|
|
// owner's to choose, as a seat chooses how long its backlog survives (design 32 §3).
|
|
type StateDeclaration struct {
|
|
Name string `json:"name"`
|
|
// History is how many values a key keeps, the current one included; zero is one.
|
|
History int `json:"history,omitempty"`
|
|
// TTLSeconds is how long a value lives once written; zero is until it is replaced or deleted.
|
|
TTLSeconds int `json:"ttl-seconds,omitempty"`
|
|
// PerMachine says the state is keyed by machine: each machine's instance writes the key named for
|
|
// its machine and no other, and the bus grants it that key alone (novox/hq ADR 0260).
|
|
PerMachine bool `json:"per-machine,omitempty"`
|
|
// KeyTTL says each key of the bucket lives the time its writer gives it, and the server writes a delete marker
|
|
// when it ends (novox/hq ADR 0433 §1): the bucket is made with the server's time to live per message.
|
|
KeyTTL bool `json:"key-ttl,omitempty"`
|
|
// ReadOnly says only the module's processes whose own bus.state names the state write it; the module's tools,
|
|
// the tool runner's account, read it and never write it (the lead's ruling, 2026-11-04: nats' warrant spend record
|
|
// `spent`). The judge's rule and the grants follow from it.
|
|
ReadOnly bool `json:"read-only,omitempty"`
|
|
// MaxMiB is the bucket's cap in MiB, between ObjectsLeastMiB and ObjectsMostMiB, as an old objects section's
|
|
// is (design 47 G §469, §695); zero is no cap, as on main.
|
|
MaxMiB int `json:"max-mib,omitempty"`
|
|
// KeptLayout is the layout its holder says it writes: its form and its catch-up tool (design 47).
|
|
KeptLayout
|
|
}
|
|
|
|
// UnmarshalJSON reads a bucket as its bare name, or as {name, history, ttl-seconds, per-machine, key-ttl, read-only, max-mib, form, catch-up}.
|
|
func (s *StateDeclaration) UnmarshalJSON(raw []byte) error {
|
|
trimmed := bytes.TrimSpace(raw)
|
|
if len(trimmed) > 0 && trimmed[0] == '"' {
|
|
return json.Unmarshal(trimmed, &s.Name)
|
|
}
|
|
type plain StateDeclaration
|
|
var full plain
|
|
var said struct {
|
|
MaxMiB *int `json:"max-mib"`
|
|
}
|
|
_ = json.Unmarshal(trimmed, &said)
|
|
dec := json.NewDecoder(bytes.NewReader(trimmed))
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(&full); err != nil {
|
|
return fmt.Errorf("a state is either a name or {name, history, ttl-seconds, per-machine, key-ttl, read-only, max-mib, form, catch-up}: %w", typedUnknown(err))
|
|
}
|
|
if err := full.KeptLayout.refuse(fmt.Sprintf("state %q", full.Name)); err != nil {
|
|
return err
|
|
}
|
|
if said.MaxMiB != nil && (*said.MaxMiB < ObjectsLeastMiB || *said.MaxMiB > ObjectsMostMiB) {
|
|
return fmt.Errorf("state %q: max-mib is between %d and %d, not %d", full.Name, ObjectsLeastMiB, ObjectsMostMiB, *said.MaxMiB)
|
|
}
|
|
*s = StateDeclaration(full)
|
|
return nil
|
|
}
|
|
|
|
// MarshalJSON writes back the short form when there is nothing else to say.
|
|
func (s StateDeclaration) MarshalJSON() ([]byte, error) {
|
|
if s.History == 0 && s.TTLSeconds == 0 && !s.PerMachine && !s.KeyTTL && !s.ReadOnly && s.MaxMiB == 0 && s.KeptLayout == (KeptLayout{}) {
|
|
return json.Marshal(s.Name)
|
|
}
|
|
type plain StateDeclaration
|
|
return json.Marshal(plain(s))
|
|
}
|
|
|
|
// SeatReadPrefix opens a read of the state a seat's holder keeps: `seat:<seat>.<state>` (design 47 G A-116, A-128).
|
|
const SeatReadPrefix = "seat:"
|
|
|
|
// SeatRead splits a read of a seat holder's state, `seat:<seat>.<state>`, into the seat and the local name; ok is false for a
|
|
// read that is not of this kind (a module's `<module>.<state>`, which ReadState splits), and err says why a `seat:` read is
|
|
// malformed. The holder is not in it: it is resolved where the read is made (seatholder), so a new holder is read with no
|
|
// change of the manifest.
|
|
func SeatRead(read string) (seat, local string, ok bool, err error) {
|
|
rest, isSeat := strings.CutPrefix(read, SeatReadPrefix)
|
|
if !isSeat {
|
|
return "", "", false, nil
|
|
}
|
|
at := strings.LastIndex(rest, ".")
|
|
if at <= 0 || at == len(rest)-1 || !stateName.MatchString(rest[:at]) || !stateName.MatchString(rest[at+1:]) {
|
|
return "", "", true, fmt.Errorf("%q does not name a seat and its state: a read of a seat holder's state is seat:<seat>.<name>", read)
|
|
}
|
|
return rest[:at], rest[at+1:], true, nil
|
|
}
|
|
|
|
// SeatViewConsume splits a consumed view of a seat, `seat:<seat>.views.<state>`, into the seat and the state; ok is false for a
|
|
// consumed entry that is not of this kind (an event, `<emitter>.<event>`), and err says why a `seat:` entry is malformed. The
|
|
// holder is not in it: the grants part resolves it from the catalog half (design 47 G §616, A-131).
|
|
func SeatViewConsume(consume string) (seat, state string, ok bool, err error) {
|
|
rest, isSeat := strings.CutPrefix(consume, SeatReadPrefix)
|
|
if !isSeat {
|
|
return "", "", false, nil
|
|
}
|
|
seat, view, named := strings.Cut(rest, ".views.")
|
|
if !named || !stateName.MatchString(seat) || !stateName.MatchString(view) {
|
|
return "", "", true, fmt.Errorf("%q does not name a seat and its view: a consumed view of a seat is seat:<seat>.views.<state>", consume)
|
|
}
|
|
return seat, view, true, nil
|
|
}
|
|
|
|
// ReadState splits a read into the owning module and the local name, or says why it is not one.
|
|
func ReadState(read string) (module, local string, err error) {
|
|
at := strings.LastIndex(read, ".")
|
|
if at <= 0 || at == len(read)-1 {
|
|
return "", "", fmt.Errorf("%q does not name a module and its state: a read is <module>.<name>", read)
|
|
}
|
|
module, local = read[:at], read[at+1:]
|
|
if !stateName.MatchString(module) {
|
|
return "", "", fmt.Errorf("%q cannot own state: a module whose state is read is one plain name", module)
|
|
}
|
|
if !stateName.MatchString(local) {
|
|
return "", "", fmt.Errorf("%q is not a state name: lower-case letters, digits and hyphens", local)
|
|
}
|
|
return module, local, nil
|
|
}
|