Files
mesh-sdk/go/catalogue/busstate.go
T
jschoubben 0bec849c40 Name the storage types, templates, options and ledger effects, and give a ledger its types (issue 1966 S2)
The words of design 25 § 12.1 and ADR 0597 §1-2 as constants, and a
ledger's types (schema, operations each with its effect, indexes) on the
bus-state entry, so the judge, the bus module and the tool runner read one
contract. No bound and no rule: those are the judge's (ADR 0578 §7).
2026-11-03 13:20:35 +01:00

208 lines
8.7 KiB
Go

package catalogue
import (
"bytes"
"encoding/json"
"fmt"
"strings"
)
// Bus state is what a module keeps on the bus, asked for as a provision of the bus module in `requires` (novox/hq ADR
// 0595 §1, design 32 § 4): an entry of `requires` in its typed long form, the local name, the storage type, the template
// and the template's options. This is the contract only (ADR 0578 §7, ADR 0593 §1): the entry's shape, decoded
// leniently. Every rule that judges an entry (a template known, an option within its bound, a fixed setting not given)
// is the indexer's judge's.
// The storage types (design 25 § 12.1): the stream is the base; the key-value bucket, the object store and the ledger
// are built on it, each with calls of its own (ADR 0595 §2).
const (
StorageStream = "stream"
StorageKeyValue = "key-value"
StorageObjectStore = "object-store"
StorageLedger = "ledger"
)
// The six templates, each a storage type with a predefined configuration (ADR 0595 §2, design 25 § 12.1): state on
// key-value, events, work queue and log on stream, objects on object store, ledger on ledger. A bare stream names
// none and gives its own Configuration.
const (
TemplateState = "state"
TemplateEvents = "events"
TemplateWorkQueue = "work-queue"
TemplateLog = "log"
TemplateObjects = "objects"
TemplateLedger = "ledger"
)
// The option names, a template's settings a module may change (design 25 § 12.1's table), written as the old sections
// wrote them, so a module that moves carries its old settings as options unchanged (ADR 0595 §11). Which option a
// template takes, and its bound, is the judge's.
const (
// OptionPastValues is how many past values a state's key keeps (the old `state`'s `history`).
OptionPastValues = "history"
// OptionTimeToLive is how long a state's value lives, in seconds.
OptionTimeToLive = "ttl-seconds"
// OptionLifetimePerKey is whether a state's key may be given a lifetime of its own.
OptionLifetimePerKey = "key-ttl"
// OptionPerMachine is whether a state is kept one bucket per machine.
OptionPerMachine = "per-machine"
// OptionAgeDays is how long a stream keeps a message, in days.
OptionAgeDays = "max-age-days"
// OptionCapMiB is the cap of a stream, a bucket or an object store, in MiB.
OptionCapMiB = "max-mib"
// OptionGrant names the trusted config value granting another module a part of it (ADR 0363 §4, ADR 0592 §9).
OptionGrant = "grant"
// OptionDeliveries is how many times a work queue's message is delivered before it is a dead letter.
OptionDeliveries = "max-deliveries"
// OptionDuplicates is how long, in seconds, a work queue detects a message sent twice.
OptionDuplicates = "duplicates"
)
// ConfigWhenFull is what a configuration of its own says a bare stream does when full: WhenFullRefuse or
// WhenFullDropOldest. In a template it is fixed, never an option (ADR 0595 §3).
const (
ConfigWhenFull = "when-full"
WhenFullRefuse = "refuse"
WhenFullDropOldest = "drop-oldest"
)
// The ledger effects, what an operation does to an instance's state (ADR 0597 §2): one of this fixed set.
const (
// EffectSetFields sets the state's fields to the operation's Fields.
EffectSetFields = "set-fields"
// EffectSetEntry sets one entry of the state's map Into, under the key the operation's field Key holds, to its Fields.
EffectSetEntry = "set-entry"
// EffectAppend appends the operation's Fields to the state's list Into.
EffectAppend = "append"
// EffectRemove removes the entry of the map Into under the operation's field Key, or, naming no Into, the instance's
// state.
EffectRemove = "remove"
)
// LedgerType is one type of a ledger (ADR 0597 §1): a schema and the operations allowed on it, with its indexes. An
// instance of it is named by a path whose first part is the type's name (`decision/0290`).
type LedgerType struct {
Name string `json:"name"`
// Schema is the type's fields, by name.
Schema map[string]LedgerField `json:"schema"`
// Operations are the operations allowed on an instance, each with its effect.
Operations []LedgerOperation `json:"operations"`
// Indexes are its declared indexes, each a second key-value bucket kept by a watcher (ADR 0597 §4).
Indexes []LedgerIndex `json:"indexes,omitempty"`
}
// LedgerField is one field of a ledger type's schema.
type LedgerField struct {
// Type is the field's value type, as the schema names it.
Type string `json:"type"`
// DigestOf names the text field this field is the digest of (ADR 0597 §2): `apply` refuses an operation whose
// digest does not match its text.
DigestOf string `json:"digest-of,omitempty"`
}
// LedgerOperation is one operation a ledger type allows, and its effect on the state.
type LedgerOperation struct {
Name string `json:"name"`
// Effect is one of the ledger effects (EffectSetFields, EffectSetEntry, EffectAppend, EffectRemove).
Effect string `json:"effect"`
// Fields are the operation's fields its effect copies into the state; it may carry more (ADR 0597 §2).
Fields []string `json:"fields,omitempty"`
// Into is the state's map (EffectSetEntry, EffectRemove) or list (EffectAppend) the effect writes.
Into string `json:"into,omitempty"`
// Key is the operation's field holding the entry's key (EffectSetEntry, EffectRemove).
Key string `json:"key,omitempty"`
// Sequence and Author are whether the effect also writes the operation's sequence and its proven author.
Sequence bool `json:"sequence,omitempty"`
Author bool `json:"author,omitempty"`
}
// LedgerIndex is one declared index of a ledger type: its name, and the fields it is kept on.
type LedgerIndex struct {
Name string `json:"name"`
On []string `json:"on"`
}
// BusStateField is where a stored manifest keeps its bus-state entries: beside `requires`, which stays a plain list of
// names, as OptionalRequiresKey keeps the optional ones.
const BusStateField = "bus-state"
// BusStateEntry is one piece of a module's bus state, as its manifest asks for it.
type BusStateEntry struct {
// Name is its local name; the mesh, never the module, names it on the bus (ADR 0201 §4).
Name string `json:"name"`
// Storage is its storage type (design 25 § 12.1): how the bus keeps it.
Storage string `json:"storage"`
// Template is the predefined configuration of that storage type it uses; empty for a bare stream, which gives
// Configuration instead.
Template string `json:"template,omitempty"`
// Options are the template's settings the module changes, by option name; each value as the manifest wrote it.
Options map[string]json.RawMessage `json:"options,omitempty"`
// Configuration is a configuration of the module's own, where no template fits: a bare stream's, or a ledger's
// whose history expires (ADR 0595 §2).
Configuration map[string]json.RawMessage `json:"configuration,omitempty"`
// Types are a ledger's types (ADR 0597 §1), for the storage type ledger only.
Types []LedgerType `json:"types,omitempty"`
}
// isBusStateEntry is whether a `requires` item in its long form asks for bus state: it names a storage type.
func isBusStateEntry(raw json.RawMessage) bool {
var keys map[string]json.RawMessage
if json.Unmarshal(raw, &keys) != nil {
return false
}
_, has := keys["storage"]
return has
}
// readBusStateEntry reads one bus-state entry leniently (ADR 0578 §3): a key it does not know, at any depth, is set
// aside and its path named, never refused. An error is only an entry of the wrong shape.
func readBusStateEntry(raw json.RawMessage) (BusStateEntry, []string, error) {
var tree any
dec := json.NewDecoder(bytes.NewReader(raw))
dec.UseNumber()
if err := dec.Decode(&tree); err != nil {
return BusStateEntry{}, nil, err
}
var aside []string
strict := func(e *BusStateEntry) error {
b, err := json.Marshal(tree)
if err != nil {
return err
}
d := json.NewDecoder(bytes.NewReader(b))
d.DisallowUnknownFields()
return typedUnknown(d.Decode(e))
}
for tries := 0; ; tries++ {
var e BusStateEntry
err := strict(&e)
if err == nil {
return e, aside, nil
}
unknown := asUnknownField(err)
if unknown == nil || tries > 64 {
return BusStateEntry{}, nil, err
}
path, ok := removalThatHelps(tree, unknown.Field, err.Error(), func() error { var e BusStateEntry; return strict(&e) })
if !ok {
if removeEvery(tree, unknown.Field) == 0 {
return BusStateEntry{}, nil, err
}
path = "…." + unknown.Field
}
aside = append(aside, path)
}
}
// setAsideAlso adds paths read without to what a manifest set aside (SetAside), in its words.
func setAsideAlso(unknown string, paths []string) string {
if len(paths) == 0 {
return unknown
}
if unknown == "" {
field := paths[0][strings.LastIndex(paths[0], ".")+1:]
unknown = fmt.Sprintf("json: unknown field %q", field)
}
return unknown + " (read without " + strings.Join(paths, ", ") + ")"
}