Merge feat/47-G-fix11-a3 (every-event and approval-mark sdk parts) into feat/47-G-fix11-b4
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
package buscompose
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The one place the mesh's event subjects are named (design 34 every-event §1, §25, §26), so neither the grants part
|
||||
// nor the tool runner spells the event namespace itself.
|
||||
|
||||
// The namespaces an event lands in, as `from` names them (design 34 every-event §13, §26).
|
||||
const (
|
||||
fromSeat = "seat"
|
||||
fromModule = "module"
|
||||
fromStream = "stream"
|
||||
)
|
||||
|
||||
// eventNamespaces are the three, each with the prefix its subjects start with, in EveryEvent's order.
|
||||
var eventNamespaces = []struct{ from, prefix string }{
|
||||
{fromSeat, "mesh.seat."},
|
||||
{fromModule, "mesh.mod."},
|
||||
{fromStream, moduleStreamPrefix},
|
||||
}
|
||||
|
||||
// EveryEvent is the subjects the tool runner's account hears: every seat's events, every module's, and every module
|
||||
// stream's, a restricted one included, in that order (novox/hq ADR 0670 §1, ADR 0671 §1).
|
||||
func EveryEvent() []string {
|
||||
out := make([]string, 0, len(eventNamespaces))
|
||||
for _, ns := range eventNamespaces {
|
||||
out = append(out, ns.prefix+"*.event.>")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// eventToken is the form of each token of an event name.
|
||||
var eventToken = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
|
||||
|
||||
// EventSubjects is the subjects one event name lands on. A name is `<emitter>.<event>`, optionally followed by further
|
||||
// tokens (`operator-channel.decided.claude-code`), each token of the form `[A-Za-z0-9_-]+`. With from empty it answers
|
||||
// six subjects, per namespace (seat, module, stream) `mesh.<ns>.<emitter>.event.<rest>` and the same followed by `.>`;
|
||||
// with from `seat`, `module` or `stream` only that namespace's two. It refuses, naming the rule, a name of fewer than two
|
||||
// tokens, an empty token or a token outside that form (so `*` and `>` are refused), and a from of any other word.
|
||||
func EventSubjects(name, from string) ([]string, error) {
|
||||
tokens := strings.Split(name, ".")
|
||||
if len(tokens) < 2 {
|
||||
return nil, fmt.Errorf("event name %q has fewer than two tokens: a name is <seat or module>.<event>", name)
|
||||
}
|
||||
for _, t := range tokens {
|
||||
if !eventToken.MatchString(t) {
|
||||
return nil, fmt.Errorf("event name %q has the token %q: each token of a name matches [A-Za-z0-9_-]+, "+
|
||||
"so no token is empty and none is * or >", name, t)
|
||||
}
|
||||
}
|
||||
emitter, rest := tokens[0], strings.Join(tokens[1:], ".")
|
||||
var out []string
|
||||
for _, ns := range eventNamespaces {
|
||||
if from != "" && from != ns.from {
|
||||
continue
|
||||
}
|
||||
subject := ns.prefix + emitter + ".event." + rest
|
||||
out = append(out, subject, subject+".>")
|
||||
}
|
||||
if out == nil {
|
||||
return nil, fmt.Errorf("from %q is none of %s, %s or %s", from, fromSeat, fromModule, fromStream)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// EventName is the inverse of EventSubjects for a heard subject: for `mesh.seat.<emitter>.event.<rest>`,
|
||||
// `mesh.mod.<emitter>.event.<rest>` or `mesh.stream.<emitter>.event.<rest>` it answers the name `<emitter>.<rest>` and
|
||||
// its namespace (`seat`, `module` or `stream`); any other subject is answered as not an event (ok false).
|
||||
func EventName(subject string) (name, from string, ok bool) {
|
||||
for _, ns := range eventNamespaces {
|
||||
after, found := strings.CutPrefix(subject, ns.prefix)
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
emitter, rest, found := strings.Cut(after, ".event.")
|
||||
if !found || emitter == "" || rest == "" || strings.Contains(emitter, ".") {
|
||||
return "", "", false
|
||||
}
|
||||
return emitter + "." + rest, ns.from, true
|
||||
}
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// Overlaps says whether some subject matches both a and b, a wildcard on either side: `*` stands for one token and
|
||||
// `>`, last, for one token or more (design 34 every-event §34).
|
||||
func Overlaps(a, b string) bool {
|
||||
at, bt := strings.Split(a, "."), strings.Split(b, ".")
|
||||
for i := 0; ; i++ {
|
||||
switch {
|
||||
case i == len(at) && i == len(bt):
|
||||
return true
|
||||
case i == len(at) || i == len(bt):
|
||||
return false
|
||||
case at[i] == ">" || bt[i] == ">":
|
||||
return true
|
||||
case at[i] == "*" || bt[i] == "*" || at[i] == bt[i]:
|
||||
continue
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package buscompose
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The mesh's event subjects are named in one place: EveryEvent, EventSubjects, EventName and Overlaps (design 34
|
||||
// every-event §1, §25, §26, §34, §37).
|
||||
func TestTheEventSubjectsAreNamedInOnePlace(t *testing.T) {
|
||||
// EE-S1/AC1 (E §1, §25): the three subjects, in order.
|
||||
if got, want := EveryEvent(), []string{"mesh.seat.*.event.>", "mesh.mod.*.event.>", "mesh.stream.*.event.>"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("EveryEvent is %v, want %v", got, want)
|
||||
}
|
||||
|
||||
// EE-S1/AC2, AC3 (E §1, §26): a name's subjects, all six or one namespace's two, and the refusals naming the rule.
|
||||
subjects := []struct {
|
||||
name, from string
|
||||
want []string
|
||||
refused string
|
||||
}{
|
||||
{name: "operator-channel.decided.claude-code", want: []string{
|
||||
"mesh.seat.operator-channel.event.decided.claude-code", "mesh.seat.operator-channel.event.decided.claude-code.>",
|
||||
"mesh.mod.operator-channel.event.decided.claude-code", "mesh.mod.operator-channel.event.decided.claude-code.>",
|
||||
"mesh.stream.operator-channel.event.decided.claude-code", "mesh.stream.operator-channel.event.decided.claude-code.>",
|
||||
}},
|
||||
{name: "mesh-delivery-view.delivered", from: "seat", want: []string{
|
||||
"mesh.seat.mesh-delivery-view.event.delivered", "mesh.seat.mesh-delivery-view.event.delivered.>"}},
|
||||
{name: "mesh-delivery-view.delivered", from: "module", want: []string{
|
||||
"mesh.mod.mesh-delivery-view.event.delivered", "mesh.mod.mesh-delivery-view.event.delivered.>"}},
|
||||
{name: "mesh-mail.mail", from: "stream", want: []string{
|
||||
"mesh.stream.mesh-mail.event.mail", "mesh.stream.mesh-mail.event.mail.>"}},
|
||||
{name: "mesh-delivery-view", refused: "fewer than two tokens"},
|
||||
{name: "mesh-delivery-view..delivered", refused: "[A-Za-z0-9_-]+"},
|
||||
{name: "mesh-delivery-view.*", refused: "[A-Za-z0-9_-]+"},
|
||||
{name: "mesh-delivery-view.>", refused: "[A-Za-z0-9_-]+"},
|
||||
{name: "mesh-delivery-view.delivered", from: "bench", refused: "none of seat, module or stream"},
|
||||
}
|
||||
for _, c := range subjects {
|
||||
got, err := EventSubjects(c.name, c.from)
|
||||
if c.refused != "" {
|
||||
if err == nil || !strings.Contains(err.Error(), c.refused) {
|
||||
t.Errorf("EventSubjects(%q, %q) answered %v, %v; want refused naming %q", c.name, c.from, got, err, c.refused)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil || !reflect.DeepEqual(got, c.want) {
|
||||
t.Errorf("EventSubjects(%q, %q) answered %v, %v; want %v", c.name, c.from, got, err, c.want)
|
||||
}
|
||||
// EE-S1/AC5 (E §1): every subject it answers is covered by one of EveryEvent's.
|
||||
for _, s := range got {
|
||||
covered := false
|
||||
for _, e := range EveryEvent() {
|
||||
covered = covered || Overlaps(e, s)
|
||||
}
|
||||
if !covered {
|
||||
t.Errorf("%s is covered by none of EveryEvent", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EE-S1/AC4 (E §1, §26): a heard subject's name and namespace, and any other subject not an event.
|
||||
names := []struct{ subject, name, from string }{
|
||||
{"mesh.seat.operator-channel.event.decided.claude-code", "operator-channel.decided.claude-code", "seat"},
|
||||
{"mesh.mod.mesh-delivery-view.event.delivered", "mesh-delivery-view.delivered", "module"},
|
||||
{"mesh.stream.mesh-delivery-checker.event.verdict", "mesh-delivery-checker.verdict", "stream"},
|
||||
{"mesh.seat.intake.call.accept.ace", "", ""},
|
||||
{"mesh.mod.netcheck.call.netcheck_dns", "", ""},
|
||||
{"$KV.views.x", "", ""},
|
||||
{"mesh.seat..event.x", "", ""},
|
||||
}
|
||||
for _, c := range names {
|
||||
name, from, ok := EventName(c.subject)
|
||||
if name != c.name || from != c.from || ok != (c.name != "") {
|
||||
t.Errorf("EventName(%q) answered %q, %q, %v; want %q, %q", c.subject, name, from, ok, c.name, c.from)
|
||||
}
|
||||
}
|
||||
|
||||
// EE-S1/AC6 (E §34, §37): overlapping subjects, a wildcard on either side.
|
||||
overlaps := []struct {
|
||||
a, b string
|
||||
want bool
|
||||
}{
|
||||
{"mesh.stream.mail.event.mail.>", "mesh.stream.mail.event.mail.*.message", true},
|
||||
{"mesh.mod.mail.event.x", "mesh.stream.mail.event.x", false},
|
||||
{"mesh.mod.mail.event.x.>", "mesh.mod.mail.event.x", false},
|
||||
{"mesh.mod.mail.event.*", "mesh.mod.*.event.x", true},
|
||||
{"mesh.mod.mail.event.x", "mesh.mod.mail.event.x", true},
|
||||
{"mesh.mod.*.event.>", "mesh.mod.mail.event.x.ace", true},
|
||||
{"mesh.mod.mail.event.x.ace", "mesh.mod.mail.event.x", false},
|
||||
}
|
||||
for _, c := range overlaps {
|
||||
if Overlaps(c.a, c.b) != c.want || Overlaps(c.b, c.a) != c.want {
|
||||
t.Errorf("Overlaps(%q, %q) is not %v both ways", c.a, c.b, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package buscompose
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// moduleStreamPrefix starts every subject a module's own stream takes.
|
||||
@@ -14,6 +15,19 @@ func ModuleStreamSubject(module, event string) string {
|
||||
return moduleStreamPrefix + module + ".event." + event
|
||||
}
|
||||
|
||||
// WirePattern is a stream's local event pattern as the bus spells it: `**` is `>`, `*` stays `*`. A reader turns a
|
||||
// stream's events into the bus's subjects with it (design 46 7-approval-mark §5): the grants part composing a stream, and
|
||||
// the tool runner deriving a restricted stream's marked subjects.
|
||||
func WirePattern(local string) string {
|
||||
parts := strings.Split(local, ".")
|
||||
for i, p := range parts {
|
||||
if p == "**" {
|
||||
parts[i] = ">"
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, ".")
|
||||
}
|
||||
|
||||
// A ModuleStream is one module's declared stream as the bus holds it.
|
||||
type ModuleStream struct {
|
||||
Module string
|
||||
|
||||
@@ -33,6 +33,16 @@ func TestTheBusNamesAgree(t *testing.T) {
|
||||
if (Bucket{}).MaxBytes() != -1 || (Bucket{MaxMiB: 2}).MaxBytes() != 2<<20 {
|
||||
t.Fatal("a bucket's cap is not its max-mib in bytes")
|
||||
}
|
||||
// Design 46 7-approval-mark §5: a stream's local event pattern as the bus spells it.
|
||||
for local, wire := range map[string]string{"mail.**": "mail.>", "mail.*.message": "mail.*.message", "sent": "sent"} {
|
||||
if got := WirePattern(local); got != wire {
|
||||
t.Errorf("WirePattern(%q) = %q, want %q", local, got, wire)
|
||||
}
|
||||
}
|
||||
// Design 46 7-approval-mark §13: a per-machine state reaches its machine's key and the keys below it, no other machine's.
|
||||
if got := PerMachineKeys("ace"); len(got) != 2 || got[0] != "ace" || got[1] != "ace.>" {
|
||||
t.Errorf("a per-machine state reaches %v on ace", got)
|
||||
}
|
||||
// A seat's family is the shared events stream filtered to its subjects: a definition says the stream's age.
|
||||
for _, st := range MeshStreams() {
|
||||
if st.Name == EventsStream && int64(st.MaxAge) != seatdef.SharedEventsMaxAgeDays*24*60*60 {
|
||||
|
||||
@@ -163,8 +163,8 @@ type Principal struct {
|
||||
// instances and read by whoever declares it.
|
||||
State []string
|
||||
Reads []string
|
||||
// PerMachine is the local names of its state keyed by machine, reaching the key named for Node and
|
||||
// no other; KeyedReads the keys of others' state it reads one key at a time (novox/hq ADR 0260).
|
||||
// PerMachine is the local names of its state keyed by machine, reaching the keys of Node (PerMachineKeys) and
|
||||
// no other machine's; KeyedReads the keys of others' state it reads one key at a time (novox/hq ADR 0260).
|
||||
PerMachine []string
|
||||
KeyedReads []KeyedRead
|
||||
// Logs is the local names of the logs this principal's module keeps (novox/hq ADR 0297).
|
||||
|
||||
@@ -18,8 +18,8 @@ type Bucket struct {
|
||||
History int
|
||||
// TTLSeconds is how long a value lives; zero is until replaced or deleted.
|
||||
TTLSeconds int
|
||||
// PerMachine says each machine's instance writes only the key named for its machine (novox/hq ADR
|
||||
// 0260), and is granted that key and no other.
|
||||
// PerMachine says each machine's instance writes only the keys of its machine (novox/hq ADR 0260; design 46
|
||||
// 7-approval-mark §13): the key named for its machine and the keys below it, PerMachineKeys, and no other machine's.
|
||||
PerMachine bool
|
||||
// KeyTTL says the bucket allows a time to live per key (novox/hq ADR 0433 §1): its owner writes each key with its
|
||||
// own, and the server writes a delete marker, kept KeyTTLMarkerTTL, when it ends.
|
||||
@@ -36,6 +36,11 @@ func (b Bucket) MaxBytes() int64 {
|
||||
return int64(b.MaxMiB) * 1024 * 1024
|
||||
}
|
||||
|
||||
// PerMachineKeys are the keys a machine's instance of a per-machine state reaches (design 46 7-approval-mark §13): the key
|
||||
// named for its machine, as every module's per-machine key keeps its form, and the keys below it, `<node>.>`; no other
|
||||
// machine's. A membership's per-machine state (membership.StateIssued's Key) reaches the same.
|
||||
func PerMachineKeys(node string) []string { return []string{node, node + ".>"} }
|
||||
|
||||
// BucketName is the bucket a module's state lives in: the module and the local name joined by an
|
||||
// underscore, which neither may contain, so two modules can never derive one bucket.
|
||||
func BucketName(module, name string) string { return module + "_" + name }
|
||||
|
||||
@@ -22,9 +22,6 @@ type Declared struct {
|
||||
Watches []Seat
|
||||
// Invokes are the tools it calls, `<module>.<tool>` or `*` (novox/hq ADR 0152).
|
||||
Invokes []string
|
||||
// Waits are the event families a session may wait on through the tool runner, the tool runner's module's
|
||||
// alone (novox/hq ADR 0367 §2): its account subscribes them for a call, through no consumer.
|
||||
Waits []Wait
|
||||
// State is the state it keeps, each a bucket its instances write (novox/hq ADR 0201).
|
||||
State []Bucket
|
||||
// Logs are the logs it keeps, each a stream its instances append to (novox/hq ADR 0297).
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package buscompose
|
||||
|
||||
// Wait is one event family the tool runner's module declares: its name as the manifest gives it, and the
|
||||
// emitter — a seat or a module — and the event, as the inventory resolved them.
|
||||
type Wait struct {
|
||||
// Name is the family as the manifest names it, `<emitter>.<event>`: what a session asks for.
|
||||
Name string
|
||||
Emitter string
|
||||
// Seat says the emitter is a seat, whose events live under the seat's namespace (novox/hq ADR 0121).
|
||||
Seat bool
|
||||
// Event is the event, with any further token the subject carries: `decided.claude-code`, the asker of a
|
||||
// seat that names it last (ADR 0259 §3).
|
||||
Event string
|
||||
// Last names what the seat's event subject ends in — "kind" for a kinded bench, "asker" for a seat naming
|
||||
// its caller (ADR 0259 §3) — and is empty for an event that ends at its name. A family needing it is waited
|
||||
// on with it given, and refused without.
|
||||
Last string
|
||||
}
|
||||
@@ -39,6 +39,11 @@ func TestAManifestMaySayWhatItInvokes(t *testing.T) {
|
||||
if back, _ := json.Marshal(runner); !strings.Contains(string(back), `"tool-runner":true`) {
|
||||
t.Errorf("a tool runner written back as %s", back)
|
||||
}
|
||||
// EE-S2/AC1: the tool runner names no events to wait on any more (novox/hq ADR 0670 §2): a stored manifest still
|
||||
// carrying `waits` is read without it, the word named unknown (design 34 every-event §2).
|
||||
if old, err := decodeForTest([]byte(`{"module":"node-tools","version":"1","tool-runner":true,"waits":["a.b"]}`)); err != nil || !strings.Contains(old.UnknownField(), `"waits"`) {
|
||||
t.Errorf("a manifest carrying waits: %v, unknown %q", err, old.UnknownField())
|
||||
}
|
||||
other, _ := decodeForTest([]byte(`{"module":"node-tools","version":"1"}`))
|
||||
if got, ok := ToolRunnerOf([]Manifest{other, runner}); !ok || got.Module != "any-name" {
|
||||
t.Errorf("the tool runner found is %q, %v: by what it says, never by its name", got.Module, ok)
|
||||
|
||||
@@ -587,13 +587,6 @@ type Manifest struct {
|
||||
// person's account (design 25 §7) already had the same shape.
|
||||
Invokes []string `json:"invokes,omitempty"`
|
||||
|
||||
// Waits are the event families a session may wait on through the mesh MCP server's `mesh_wait`, each
|
||||
// `<emitter>.<event>` as `consumes` names one, a seat's or a module's (novox/hq ADR 0367 §2). Only the tool
|
||||
// runner's module declares them, one by one and never by a wildcard. **A grant apart from `consumes`**: the
|
||||
// controller grants the tool runner's account a plain subscription on those subjects alone, with no
|
||||
// durable consumer, and issues the same list in its membership; no call or setting adds one (§6).
|
||||
Waits []string `json:"waits,omitempty"`
|
||||
|
||||
// State is the current state this module keeps on the bus, by local name: each a key-value
|
||||
// bucket the controller creates, which every instance of the module writes and reads
|
||||
// (novox/hq ADR 0201). Not history — that is an event — and never a secret, sealed or not.
|
||||
@@ -812,7 +805,7 @@ type Manifest struct {
|
||||
OwnSecrets OwnSecrets `json:"own-secrets,omitempty"`
|
||||
|
||||
// ToolRunner says this module is the node's tool runner (issue 774): the one program on each node that loads every
|
||||
// assigned module's tools bundle, served by its runtime process and the only module that may declare `waits`. Said by
|
||||
// assigned module's tools bundle, served by its runtime process. Said by
|
||||
// the module, never by a name in mesh-sdk.
|
||||
ToolRunner bool `json:"tool-runner,omitempty"`
|
||||
|
||||
|
||||
@@ -49,8 +49,9 @@ func TestOwnManifestUsesNoWordTheRunningControllerRefuses(t *testing.T) {
|
||||
}
|
||||
}
|
||||
// The two seats the milestone renamed (ADR 0634 §9) are read by their new names; a node-engine built with the baseline
|
||||
// reads the old ones, and a manifest of the new names is the milestone's to deliver after the engines.
|
||||
renamed := []string{"seat:mesh-enrolment", "seat:mesh-placement"}
|
||||
// reads the old ones, and a manifest of the new names is the milestone's to deliver after the engines. The word
|
||||
// `waits` is no longer read: novox/hq ADR 0670 §2 retired it.
|
||||
renamed := []string{"seat:mesh-enrolment", "seat:mesh-placement", "word:waits"}
|
||||
for _, b := range baseline {
|
||||
if !slices.Contains(reads, b) && !slices.Contains(renamed, b) {
|
||||
t.Errorf("the baseline's %s is no reader capability of this contract", b)
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// novox/hq ADR 0367 §2: the tool runner's module names, one by one, the event families a session may wait on.
|
||||
func TestTheToolRunnerMayNameTheEventsASessionWaitsOn(t *testing.T) {
|
||||
m, err := decodeForTest([]byte(`{"module":"node-tools","version":"1",` +
|
||||
`"waits":["mesh-knowledge.reviewed","operator-channel.decided.claude-code"]}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(m.Waits) != 2 || m.Waits[1] != "operator-channel.decided.claude-code" {
|
||||
t.Fatalf("waits not read: %v", m.Waits)
|
||||
}
|
||||
}
|
||||
@@ -61,8 +61,6 @@ type Membership struct {
|
||||
// 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"`
|
||||
@@ -112,7 +110,8 @@ type SeatServed struct {
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Key, when set, names the machine whose keys it may reach (novox/hq ADR 0260; design 46 7-approval-mark §13): that key and the
|
||||
// keys below it (buscompose.PerMachineKeys), no other 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.
|
||||
@@ -174,12 +173,6 @@ type HeardIssued struct {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user