Files
mesh-sdk/go/membership/membership.go
T
jschoubben 17475257d1
mesh/delivery merged: source.main moved to novox/mesh-sdk@017743219669
mesh/delivery-group group fix/1966-requires-state merged: every member is merged
mesh/check checking 17475257d15f on 017743219669 | novox/mesh-sdk#177 in group fix/1966-requires-state
One rule for the state a membership issues and a manifest keeps, in both forms
A key-value entry asked in requires (ADR 0595) was bound but never issued
as a state, because each reader read the old state list alone (issue 1966).
Membership.StateIssued and StateNames, and catalogue.KeptStates, are the one
rule the tool runner, the grants part and the manifest validator call.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-09 12:34:36 +01:00

211 lines
9.5 KiB
Go

// Package membership is what the mesh issues an assignment to serve and to reach (novox/hq ADR 0160), as a
// process reads it from the bus: the Membership shape, the one subject it is published on, the stream that
// holds it, and the bus address helpers a reader needs. The controller composes it and the route proxy reads
// it; both use this one definition, so there is no second copy to drift (novox/hq issue 1633).
package membership
import (
"encoding/json"
"slices"
"sort"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// AssignmentsStream holds every assignment's membership, the newest per subject.
const AssignmentsStream = "ASSIGNMENTS"
// Subject is the one address a runtime derives for itself: where its own membership is published, from
// the two names its credential carries. Everything else is in the membership.
func Subject(node, module string) string {
return "mesh.assignment." + node + "." + module
}
// DirectGetSubject is the subject-addressed direct get of a membership from its stream: the one request an
// account may make of the stream.
func DirectGetSubject(node, module string) string {
return "$JS.API.DIRECT.GET." + AssignmentsStream + "." + Subject(node, module)
}
// Membership is one assignment's subjects: what this instance of a module on this machine serves,
// and what it may reach.
type Membership struct {
Node string `json:"node"`
Module string `json:"module"`
// Serves is every address a tool of this instance answers on. `{tool}` stands for the tool's
// own name. An address with a queue is shared with the module's other instances.
Serves []Served `json:"serves"`
// Seats is every verb of a seat this instance holds, at the subject the seat's callers use.
Seats []SeatServed `json:"seats,omitempty"`
// Emits is where an event of this module lands; `{event}` stands for the event's name.
Emits string `json:"emits"`
// Reaches is each tool this module may call, `<module>.<tool>`, to the subjects that reach it.
Reaches map[string][]string `json:"reaches,omitempty"`
// Tools is where this instance answers what it serves.
Tools string `json:"tools"`
// Receives is what this assignment is given for each requirement it receives, by requirement (novox/hq
// ADR 0167): kept as JSON because the catalogue owns the shape of a contribution.
Receives map[string]json.RawMessage `json:"receives,omitempty"`
// Mesh is every machine's address on the private network (novox/hq ADR 0167).
Mesh []string `json:"mesh,omitempty"`
// State is every bucket this module's code may reach, and whether it may write it (novox/hq ADR 0201).
State []StateIssued `json:"state,omitempty"`
// Logs is every log this module's code may reach (novox/hq ADR 0297).
Logs []LogIssued `json:"logs,omitempty"`
// SeatTraffic is what this module's code may submit, say, hear, take, ask, answer and read on seats
// that name their caller or their kind (novox/hq ADR 0259 §3).
SeatTraffic *SeatTraffic `json:"seat-traffic,omitempty"`
// Streams are this module's own streams, Hears the module streams it hears (novox/hq ADR 0363).
Streams []StreamIssued `json:"streams,omitempty"`
Hears []HeardIssued `json:"hears,omitempty"`
// Waits are the event families a session may wait on through the mesh MCP server (novox/hq ADR 0367 §2).
Waits []WaitIssued `json:"waits,omitempty"`
// Bindings are the module's bus state as the bus module created it, one per entry its manifest asks for in
// `requires` (novox/hq ADR 0595 §1, §7): written by the grants part when it hears the bus module's `created`.
Bindings []BusBinding `json:"bindings,omitempty"`
}
// BusBinding is one bus-state entry's names on the bus, by the local name its module asks for it by (novox/hq ADR 0595
// §1, design 25 § 12.2): the module names only locally, and reaches its own entries, and only those, through these.
// Which names are set follows the storage type: a stream's Stream and Subject, a key-value bucket's Bucket, an object
// store's ObjectStore, and a ledger's History, State, Indexes and Snapshot.
type BusBinding struct {
Local string `json:"local"`
// Storage and Template are the entry's storage type and template (catalogue.Storage…, catalogue.Template…).
Storage string `json:"storage"`
Template string `json:"template,omitempty"`
// Stream and Subject are a stream's name and the subject its messages are on; Subject is also a ledger's history's.
Stream string `json:"stream,omitempty"`
Subject string `json:"subject,omitempty"`
// Bucket is a key-value bucket's name.
Bucket string `json:"bucket,omitempty"`
// ObjectStore is an object store's name.
ObjectStore string `json:"object-store,omitempty"`
// Ledger, for the storage type ledger: its history stream (one subject per instance), its state bucket, each
// declared index's bucket by index name, and the subject of the instances' snapshots (ADR 0597 §1, §4, §8).
History string `json:"history,omitempty"`
State string `json:"state,omitempty"`
Indexes map[string]string `json:"indexes,omitempty"`
Snapshot string `json:"snapshot,omitempty"`
// Types are a ledger's types, copied by the grants part from the entry the manifest-accepted event carries, so the
// tool runner checks an operation against its type without reading another part's state (ADR 0596, ADR 0597 §1).
Types []catalogue.LedgerType `json:"types,omitempty"`
}
// Served is one address a tool is answered on.
type Served struct {
Subject string `json:"subject"`
Queue string `json:"queue,omitempty"`
}
// SeatServed is one verb of a held seat, where its callers ask.
type SeatServed struct {
Seat string `json:"seat"`
Verb string `json:"verb"`
Subject string `json:"subject"`
}
// StateIssued is one bucket an assignment may reach, by the name its module uses for it (novox/hq ADR 0201).
// Key, when set, is the one key it may reach (novox/hq ADR 0260): this machine's.
//
// PerNode says the state is kept one bucket per node (novox/hq ADR 0593 §6: `composed-<node>`): Bucket is then the
// buckets' prefix, each bucket the prefix and a node's name, and a key's first part names its node and so its bucket.
// The runtime reads it as one state (as process.NodeFamily does): a key from its node's bucket, the keys and a watch over
// every bucket of the family, one created later included. Read only: PerNode is never issued with Writes.
type StateIssued struct {
Name string `json:"name"`
Bucket string `json:"bucket"`
Writes bool `json:"writes,omitempty"`
Key string `json:"key,omitempty"`
PerNode bool `json:"per-node,omitempty"`
}
// LogIssued is one log an assignment may reach, by the name its module uses for it (novox/hq ADR 0297).
type LogIssued struct {
Name string `json:"name"`
Stream string `json:"stream"`
Subject string `json:"subject"`
Writes bool `json:"writes"`
}
// Worker is one durable consumer a holder pulls a seat's work from.
type Worker struct {
Stream string
Consumer string
Filter string
}
// SeatTraffic is one module's seat traffic beyond tools. Publish and Subscribe are subject patterns, in the
// server's wildcards.
type SeatTraffic struct {
Publish []string `json:"publish,omitempty"`
Subscribe []string `json:"subscribe,omitempty"`
Answers []string `json:"answers,omitempty"`
Workers []Worker `json:"workers,omitempty"`
Records []string `json:"records,omitempty"`
Kinds []KindHeld `json:"kinds,omitempty"`
}
// KindHeld is one kind of a kinded bench and who holds it.
type KindHeld struct {
Seat string `json:"seat"`
Kind string `json:"kind"`
Module string `json:"module"`
Node string `json:"node"`
Capabilities []string `json:"capabilities,omitempty"`
}
// StreamIssued is one of a module's own streams as its membership names it (ADR 0363 §2).
type StreamIssued struct {
Stream string `json:"stream"`
Events []string `json:"events"`
Subject string `json:"subject"`
}
// HeardIssued is one module stream an assignment hears (ADR 0363 §8).
type HeardIssued struct {
Stream string `json:"stream"`
Consumer string `json:"consumer"`
}
// WaitIssued is one family as a membership issues it: the name a session asks for, and its subject.
type WaitIssued struct {
Event string `json:"event"`
Subject string `json:"subject"`
}
// StateIssued is the state this membership issues its module's code by a local name, and whether it does: what every
// runtime that serves a module's state calls reads, so no reader keeps a second copy of the rule (novox/hq issue 1966).
// An entry of the old `state` list wins (ADR 0201); otherwise a binding of a key-value entry the module asks for in
// `requires` is issued by its local name, as its own and written (ADR 0595 §1, §11: a moved module keeps its local
// names), on the bucket the bus module named.
func (m Membership) StateIssued(name string) (StateIssued, bool) {
for _, s := range m.State {
if s.Name == name {
return s, true
}
}
for _, b := range m.Bindings {
if b.Local == name && b.Storage == catalogue.StorageKeyValue && b.Bucket != "" {
return StateIssued{Name: b.Local, Bucket: b.Bucket, Writes: true}, true
}
}
return StateIssued{}, false
}
// StateNames are the local names StateIssued answers, the old list's and the key-value bindings', sorted, once each:
// what a refusal names as issued.
func (m Membership) StateNames() []string {
var out []string
for _, s := range m.State {
out = append(out, s.Name)
}
for _, b := range m.Bindings {
if b.Storage == catalogue.StorageKeyValue && b.Bucket != "" {
out = append(out, b.Local)
}
}
sort.Strings(out)
return slices.Compact(out)
}