Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1337bbbf2 | ||
|
|
ade07bfdba | ||
|
|
07ab8069a4 | ||
|
|
b90376ba59 |
+1
-1
@@ -8,7 +8,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261023150321-a2c813a6b468
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030100935-c2ec8b6b2fde
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261023150321-a2c813a6b468 h1:UcMee+IV2ii5E4dRZdQUaSaeh9IedTFYumuQbBiB0k0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261023150321-a2c813a6b468/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030095131-16bc0dcf3e9d h1:jFOz6wUfZvIayd4DctwlaFKKZHQNME6NLs2OS1rNv30=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030095131-16bc0dcf3e9d/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030095816-530d8d3007dc h1:Wdjl2gomooHswJ5qpkwZPq/ureZgXVw6FMCsgzPJhPo=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030095816-530d8d3007dc/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030100935-c2ec8b6b2fde h1:eKXSllHCS31GgMazfvZ7BeiLoXIUibnUmzxWVkj+Vcw=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030100935-c2ec8b6b2fde/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -221,49 +221,17 @@ func (c *Conn) LogRead(module, name string, from uint64, keys string, limit int)
|
||||
if from == 0 {
|
||||
from = 1
|
||||
}
|
||||
req, _ := json.Marshal(map[string]any{"seq": from, "next_by_subj": filter, "batch": limit, "max_bytes": LogPageBytes})
|
||||
inbox := c.nc.NewInbox()
|
||||
sub, err := c.nc.SubscribeSync(inbox)
|
||||
msgs, next, err := c.directBatch(l.Stream, filter, from, limit, name, func() error { return notOnTheBus(l) })
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = sub.Unsubscribe() }()
|
||||
if err := c.nc.PublishRequest("$JS.API.DIRECT.GET."+l.Stream, inbox, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
page := &LogPage{Entries: []LogEntry{}}
|
||||
deadline := time.Now().Add(StateTimeout)
|
||||
for done := false; !done; {
|
||||
msg, err := sub.NextMsg(time.Until(deadline))
|
||||
page := &LogPage{Entries: []LogEntry{}, Next: next}
|
||||
for _, msg := range msgs {
|
||||
e, err := logEntryOf(l, msg)
|
||||
if err != nil {
|
||||
if errors.Is(err, nats.ErrNoResponders) {
|
||||
return nil, notOnTheBus(l)
|
||||
}
|
||||
if errors.Is(err, nats.ErrTimeout) {
|
||||
return nil, fmt.Errorf("the read of %s did not end in %s", name, StateTimeout)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
switch status := msg.Header.Get("Status"); status {
|
||||
case "":
|
||||
e, err := logEntryOf(l, msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
page.Entries = append(page.Entries, *e)
|
||||
case "204": // the end of the batch: what is still pending after the last entry sent
|
||||
pending, _ := strconv.ParseUint(msg.Header.Get("Nats-Num-Pending"), 10, 64)
|
||||
if pending > 0 && len(page.Entries) > 0 {
|
||||
page.Next = page.Entries[len(page.Entries)-1].Sequence + 1
|
||||
}
|
||||
done = true
|
||||
case "404": // nothing from that sequence on
|
||||
done = true
|
||||
case "503":
|
||||
return nil, notOnTheBus(l)
|
||||
default:
|
||||
return nil, fmt.Errorf("the read of %s was refused by the bus: %s %s", name, status, msg.Header.Get("Description"))
|
||||
}
|
||||
page.Entries = append(page.Entries, *e)
|
||||
}
|
||||
info, err := c.logInfo(l)
|
||||
if err != nil {
|
||||
@@ -273,6 +241,58 @@ func (c *Conn) LogRead(module, name string, from uint64, keys string, limit int)
|
||||
return page, nil
|
||||
}
|
||||
|
||||
// directBatch is the messages of a stream from a sequence on, on the subjects matching filter, at most limit and about
|
||||
// LogPageBytes, by one batch direct get (NATS 2.11), and the sequence to read on from, 0 when nothing follows. name is
|
||||
// what is read, for its errors; missing is the error of a stream that is not on the bus.
|
||||
func (c *Conn) directBatch(stream, filter string, from uint64, limit int, name string, missing func() error) ([]*nats.Msg, uint64, error) {
|
||||
if from == 0 {
|
||||
from = 1
|
||||
}
|
||||
req, _ := json.Marshal(map[string]any{"seq": from, "next_by_subj": filter, "batch": limit, "max_bytes": LogPageBytes})
|
||||
inbox := c.nc.NewInbox()
|
||||
sub, err := c.nc.SubscribeSync(inbox)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer func() { _ = sub.Unsubscribe() }()
|
||||
if err := c.nc.PublishRequest("$JS.API.DIRECT.GET."+stream, inbox, req); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var msgs []*nats.Msg
|
||||
var next uint64
|
||||
deadline := time.Now().Add(StateTimeout)
|
||||
for done := false; !done; {
|
||||
msg, err := sub.NextMsg(time.Until(deadline))
|
||||
if err != nil {
|
||||
if errors.Is(err, nats.ErrNoResponders) {
|
||||
return nil, 0, missing()
|
||||
}
|
||||
if errors.Is(err, nats.ErrTimeout) {
|
||||
return nil, 0, fmt.Errorf("the read of %s did not end in %s", name, StateTimeout)
|
||||
}
|
||||
return nil, 0, err
|
||||
}
|
||||
switch status := msg.Header.Get("Status"); status {
|
||||
case "":
|
||||
msgs = append(msgs, msg)
|
||||
case "204": // the end of the batch: what is still pending after the last message sent
|
||||
pending, _ := strconv.ParseUint(msg.Header.Get("Nats-Num-Pending"), 10, 64)
|
||||
if pending > 0 && len(msgs) > 0 {
|
||||
last, _ := strconv.ParseUint(msgs[len(msgs)-1].Header.Get("Nats-Sequence"), 10, 64)
|
||||
next = last + 1
|
||||
}
|
||||
done = true
|
||||
case "404": // nothing from that sequence on
|
||||
done = true
|
||||
case "503":
|
||||
return nil, 0, missing()
|
||||
default:
|
||||
return nil, 0, fmt.Errorf("the read of %s was refused by the bus: %s %s", name, status, msg.Header.Get("Description"))
|
||||
}
|
||||
}
|
||||
return msgs, next, nil
|
||||
}
|
||||
|
||||
// LogLast is a key's last entry, or nil when it has none.
|
||||
func (c *Conn) LogLast(module, name, key string) (*LogEntry, error) {
|
||||
l, err := c.issuedLog(module, name)
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package bus
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// A module reading its own stream back (novox/hq ADR 0363; the operator, 2026-10-30: a build step's whole failure output
|
||||
// is said on its build module's stream, and the module's tools read it there). As a log's read: one batch direct get,
|
||||
// no consumer, and only a stream the module's own membership names (`streams`); the bus grants its owner that stream's
|
||||
// info and direct get and no other's (mesh-sdk buscompose ownStreamGrants).
|
||||
|
||||
// StreamEntry is one event of a module's own stream: its sequence, its event (the subject after the stream's
|
||||
// `…event.`), its body and when the bus took it.
|
||||
type StreamEntry struct {
|
||||
Sequence uint64 `json:"sequence"`
|
||||
Event string `json:"event"`
|
||||
Body json.RawMessage `json:"body"`
|
||||
At time.Time `json:"at"`
|
||||
}
|
||||
|
||||
// StreamPage is one page of a stream's read: Next is the sequence to continue from, 0 when nothing follows; Last the
|
||||
// stream's last sequence.
|
||||
type StreamPage struct {
|
||||
Entries []StreamEntry `json:"entries"`
|
||||
Next uint64 `json:"next"`
|
||||
Last uint64 `json:"last"`
|
||||
}
|
||||
|
||||
// streamBusName is a stream's local name as the bus holds it (mesh-sdk catalogue.StreamBusName): capitals, a hyphen an
|
||||
// underscore; a bus name is itself.
|
||||
func streamBusName(name string) string { return strings.ToUpper(strings.ReplaceAll(name, "-", "_")) }
|
||||
|
||||
// issuedStream is the stream of its own a module may read by a name, local or the bus's, or the reason it may not.
|
||||
func (c *Conn) issuedStream(module, name string) (StreamIssued, error) {
|
||||
m := c.Membership(module)
|
||||
if m == nil {
|
||||
return StreamIssued{}, fmt.Errorf("%s has no membership issued on %s yet, so no stream of it is readable "+
|
||||
"until the mesh issues one (novox/hq ADR 0363)", module, c.node)
|
||||
}
|
||||
var names []string
|
||||
for _, s := range m.Streams {
|
||||
if s.Stream == streamBusName(name) && strings.Contains(s.Subject, "{event}") {
|
||||
return s, nil
|
||||
}
|
||||
names = append(names, s.Stream)
|
||||
}
|
||||
sort.Strings(names)
|
||||
issued := "none"
|
||||
if len(names) > 0 {
|
||||
issued = strings.Join(names, ", ")
|
||||
}
|
||||
return StreamIssued{}, fmt.Errorf("%s keeps no stream called %q: a module reads the streams it declares under `streams`, "+
|
||||
"and no other's; it was issued: %s (novox/hq ADR 0363)", module, name, issued)
|
||||
}
|
||||
|
||||
// StreamRead is a module's own stream's events from a sequence on, whose event names match a pattern (`*` one name, `>`
|
||||
// the rest, last; empty is every event), at most limit (LogReadDefault when 0, at most LogReadMax) and about LogPageBytes.
|
||||
func (c *Conn) StreamRead(module, name, events string, from uint64, limit int) (*StreamPage, error) {
|
||||
s, err := c.issuedStream(module, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch {
|
||||
case limit == 0:
|
||||
limit = LogReadDefault
|
||||
case limit < 0 || limit > LogReadMax:
|
||||
return nil, fmt.Errorf("a read's limit is 1 to %d events, %d when none is given, not %d", LogReadMax, LogReadDefault, limit)
|
||||
}
|
||||
if events == "" {
|
||||
events = ">"
|
||||
}
|
||||
parts := strings.Split(events, ".")
|
||||
for i, p := range parts {
|
||||
if !(p == ">" && i == len(parts)-1) && p != "*" && !logToken.MatchString(p) {
|
||||
return nil, fmt.Errorf("%q is not an event pattern: names, `*` for one and `>` for the rest, last", events)
|
||||
}
|
||||
}
|
||||
prefix, _, _ := strings.Cut(s.Subject, "{event}")
|
||||
missing := func() error {
|
||||
return fmt.Errorf("the stream %s is issued and not on the bus yet: the controller creates it from the catalogue "+
|
||||
"on its next raise, and reads it directly once it allows that (novox/hq ADR 0363)", s.Stream)
|
||||
}
|
||||
msgs, next, err := c.directBatch(s.Stream, prefix+events, from, limit, s.Stream, missing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
page := &StreamPage{Entries: []StreamEntry{}, Next: next}
|
||||
for _, msg := range msgs {
|
||||
e, err := streamEntryOf(s.Stream, prefix, msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
page.Entries = append(page.Entries, *e)
|
||||
}
|
||||
info, err := c.logInfo(LogIssued{Name: s.Stream, Stream: s.Stream})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
page.Last = info.Last
|
||||
return page, nil
|
||||
}
|
||||
|
||||
// streamEntryOf is an event a direct get answered: its event is its subject after prefix.
|
||||
func streamEntryOf(stream, prefix string, msg *nats.Msg) (*StreamEntry, error) {
|
||||
subject := msg.Header.Get("Nats-Subject")
|
||||
event, ok := strings.CutPrefix(subject, prefix)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("the bus answered an event of %s on %q, which is not under %s", stream, subject, prefix)
|
||||
}
|
||||
seq, err := strconv.ParseUint(msg.Header.Get("Nats-Sequence"), 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the bus answered an event of %s with no sequence", stream)
|
||||
}
|
||||
at, err := time.Parse(time.RFC3339Nano, msg.Header.Get("Nats-Time-Stamp"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the bus answered event %d of %s with no time: %v", seq, stream, err)
|
||||
}
|
||||
return &StreamEntry{Sequence: seq, Event: event, Body: valueOf(msg.Data), At: at.UTC()}, nil
|
||||
}
|
||||
@@ -25,17 +25,8 @@ func TestTheManifestNamesTheFirstFamiliesOneByOne(t *testing.T) {
|
||||
if err := json.Unmarshal(raw, &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 2026-10-30, the operator: "an approval given by me must get handled accordingly and you should know about it
|
||||
// immediately" and "you should be able to hook into approval requests": the gate's approval (approved, not handed),
|
||||
// and every asker's asks opening and decided, one family per asker.
|
||||
want := []string{"mesh-knowledge.reviewed", "mesh-delivery-gate.transition", "gitea.forge.status",
|
||||
"mesh-controller.checked", "operator-channel.decided.claude-code", "mesh-delivery-gate.approval"}
|
||||
for _, asker := range []string{"mesh-config", "mesh-delivery-assigner", "mesh-delivery-teller", "mesh-grants", "mesh-knowledge", "mesh-mail", "pacman"} {
|
||||
want = append(want, "operator-channel.decided."+asker)
|
||||
}
|
||||
for _, asker := range []string{"claude-code", "mesh-config", "mesh-delivery-assigner", "mesh-delivery-teller", "mesh-grants", "mesh-knowledge", "mesh-mail", "pacman"} {
|
||||
want = append(want, "operator-channel.asked."+asker)
|
||||
}
|
||||
"mesh-controller.checked", "operator-channel.decided.claude-code"}
|
||||
if m.Module != "node-tools" || !reflect.DeepEqual(m.Waits, want) {
|
||||
t.Errorf("%s waits on %v, want %v", m.Module, m.Waits, want)
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ func (b *subscribing) Publish(json.RawMessage) error {
|
||||
func (b *subscribing) Ask(json.RawMessage) (json.RawMessage, error) { return nil, nil }
|
||||
func (b *subscribing) State(string, json.RawMessage) (json.RawMessage, error) { return nil, nil }
|
||||
func (b *subscribing) Log(string, json.RawMessage) (json.RawMessage, error) { return nil, nil }
|
||||
func (b *subscribing) StreamRead(json.RawMessage) (json.RawMessage, error) { return nil, nil }
|
||||
func (b *subscribing) Watch(json.RawMessage, func(json.RawMessage) error) (func(), error) {
|
||||
return func() {}, nil
|
||||
}
|
||||
|
||||
@@ -80,6 +80,9 @@ type Bus interface {
|
||||
Watch(params json.RawMessage, deliver func(change json.RawMessage) error) (stop func(), err error)
|
||||
// Log answers a bundle's `append`, `read`, `last` and `info` on its module's log (novox/hq ADR 0297).
|
||||
Log(verb string, params json.RawMessage) (json.RawMessage, error)
|
||||
// StreamRead answers a bundle's `read` of one of its module's own streams (novox/hq ADR 0363): {stream, subject,
|
||||
// from, limit}, as a log's read.
|
||||
StreamRead(params json.RawMessage) (json.RawMessage, error)
|
||||
// Seat answers a bundle's seat traffic (novox/hq ADR 0259 §3): `publish`, `prove` and `record` at once;
|
||||
// `take` and `answer` bind once and hand each message or proof on through handOn, to whichever child
|
||||
// of the module asked last — a restarted child asks again as its code runs again.
|
||||
@@ -333,6 +336,11 @@ func Start(module, entry string, env []string, mesh Bus, logf func(string, ...an
|
||||
if answered, err = mesh.Log(strings.TrimPrefix(m.Method, "mesh/log."), m.Params); err == nil {
|
||||
result = answered
|
||||
}
|
||||
case "mesh/stream.read":
|
||||
var answered json.RawMessage
|
||||
if answered, err = mesh.StreamRead(m.Params); err == nil {
|
||||
result = answered
|
||||
}
|
||||
case "mesh/provision.get":
|
||||
var answered json.RawMessage
|
||||
if answered, err = mesh.Provision("get", m.Params); err == nil {
|
||||
|
||||
@@ -278,7 +278,7 @@ func (m *Mesh) ModuleStream(t *testing.T, module, name string) {
|
||||
t.Helper()
|
||||
_ = m.js.DeleteStream(name)
|
||||
if _, err := m.js.AddStream(&nats.StreamConfig{Name: name, Subjects: []string{"mesh.stream." + module + ".event.>"},
|
||||
Retention: nats.LimitsPolicy, Discard: nats.DiscardOld}); err != nil {
|
||||
Retention: nats.LimitsPolicy, Discard: nats.DiscardOld, AllowDirect: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ func logBuses(t *testing.T, maxBytes int64) (issues, other launch.Bus, mesh *mt.
|
||||
mesh = mt.New(t)
|
||||
m := mt.MembershipOf("issues", "anchor", false, nil)
|
||||
m.Logs = []bus.LogIssued{mesh.Log(t, "issues", "changes", maxBytes)}
|
||||
mesh.ModuleStream(t, "issues", "ISSUE_MOVES")
|
||||
m.Streams = []bus.StreamIssued{{Stream: "ISSUE_MOVES", Events: []string{"moved.>"}, Subject: "mesh.stream.issues.event.{event}"}}
|
||||
mesh.Issue(t, m)
|
||||
mesh.Issue(t, mt.MembershipOf("other", "anchor", false, nil))
|
||||
conn := connect(t, "node-tools", "anchor")
|
||||
@@ -111,7 +113,7 @@ func infoOf(t *testing.T, b launch.Bus) map[string]uint64 {
|
||||
// RFC 3339; last is a key's last entry or null; info says entries, bytes, cap, first and last — and no
|
||||
// consumer is made for any of it.
|
||||
func TestALogAnswersEachVerbOnTheBus(t *testing.T) {
|
||||
b, _, mesh := logBuses(t, 1<<20)
|
||||
b, other, mesh := logBuses(t, 1<<20)
|
||||
before := time.Now().Add(-time.Second)
|
||||
|
||||
var seqs []uint64
|
||||
@@ -189,6 +191,45 @@ func TestALogAnswersEachVerbOnTheBus(t *testing.T) {
|
||||
if n := mesh.Consumers(t, "LOG_issues_changes"); n != 0 {
|
||||
t.Fatalf("the runtime made %d consumers of the log; it uses none", n)
|
||||
}
|
||||
|
||||
// A module reads its own stream back as it reads its log (mesh/stream.read): by its local name, an event pattern,
|
||||
// in pages; another module's stream is refused with the reason.
|
||||
for i, ev := range []string{"moved.a.1", "moved.b.1", "moved.a.1"} {
|
||||
if err := b.Publish(json.RawMessage(fmt.Sprintf(`{"key":%q,"body":{"n":%d}}`, ev, i))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
streamOf := func(b launch.Bus, params string) (wireStream, error) {
|
||||
var p wireStream
|
||||
raw, err := b.StreamRead(json.RawMessage(params))
|
||||
if err == nil {
|
||||
err = json.Unmarshal(raw, &p)
|
||||
}
|
||||
return p, err
|
||||
}
|
||||
page, err := streamOf(b, `{"stream":"issue-moves","subject":"moved.a.>","limit":1}`)
|
||||
if err != nil || len(page.Entries) != 1 || page.Entries[0].Event != "moved.a.1" || string(page.Entries[0].Body) != `{"n":0}` || page.Next == 0 || page.Last != 3 {
|
||||
t.Fatalf("the first page of moved.a: %+v, %v", page, err)
|
||||
}
|
||||
page, err = streamOf(b, fmt.Sprintf(`{"stream":"ISSUE_MOVES","subject":"moved.a.>","from":%d}`, page.Next))
|
||||
if err != nil || len(page.Entries) != 1 || string(page.Entries[0].Body) != `{"n":2}` || page.Next != 0 {
|
||||
t.Fatalf("the second page of moved.a: %+v, %v", page, err)
|
||||
}
|
||||
if _, err := streamOf(other, `{"stream":"issue-moves"}`); err == nil || !strings.Contains(err.Error(), "keeps no stream") {
|
||||
t.Fatalf("another module's stream: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// wireStream is a stream read's answer with exactly the contract's field names (mesh-sdk stdio.StreamPage).
|
||||
type wireStream struct {
|
||||
Entries []struct {
|
||||
Sequence uint64 `json:"sequence"`
|
||||
Event string `json:"event"`
|
||||
Body json.RawMessage `json:"body"`
|
||||
At string `json:"at"`
|
||||
} `json:"entries"`
|
||||
Next uint64 `json:"next"`
|
||||
Last uint64 `json:"last"`
|
||||
}
|
||||
|
||||
// A page holds about 4 MiB at most, whatever its limit, and says where to continue.
|
||||
|
||||
@@ -988,6 +988,26 @@ func (b *moduleBus) Log(verb string, params json.RawMessage) (json.RawMessage, e
|
||||
return json.Marshal(answer)
|
||||
}
|
||||
|
||||
// StreamRead answers a bundle's read of one of its module's own streams (ADR 0363): {stream, subject, from, limit}, the
|
||||
// stream by its local name, the subject an event pattern. The runtime refuses, with the reason, a stream the module's
|
||||
// membership does not name.
|
||||
func (b *moduleBus) StreamRead(params json.RawMessage) (json.RawMessage, error) {
|
||||
var asked struct {
|
||||
Stream string `json:"stream"`
|
||||
Subject string `json:"subject"`
|
||||
From uint64 `json:"from"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
if err := json.Unmarshal(params, &asked); err != nil || asked.Stream == "" {
|
||||
return nil, fmt.Errorf("mesh/stream.read names no stream: {stream, subject, from, limit}")
|
||||
}
|
||||
page, err := b.all.conn.StreamRead(b.module, asked.Stream, asked.Subject, asked.From, asked.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(page)
|
||||
}
|
||||
|
||||
// Watch hands a bundle its module's state as it is and as it changes (ADR 0201): `{state, key}`, the
|
||||
// key a pattern with `*` and `**`, empty for every key.
|
||||
func (b *moduleBus) Watch(params json.RawMessage, deliver func(json.RawMessage) error) (func(), error) {
|
||||
|
||||
+1
-17
@@ -10,23 +10,7 @@
|
||||
"mesh-delivery-gate.transition",
|
||||
"gitea.forge.status",
|
||||
"mesh-controller.checked",
|
||||
"operator-channel.decided.claude-code",
|
||||
"mesh-delivery-gate.approval",
|
||||
"operator-channel.decided.mesh-config",
|
||||
"operator-channel.decided.mesh-delivery-assigner",
|
||||
"operator-channel.decided.mesh-delivery-teller",
|
||||
"operator-channel.decided.mesh-grants",
|
||||
"operator-channel.decided.mesh-knowledge",
|
||||
"operator-channel.decided.mesh-mail",
|
||||
"operator-channel.decided.pacman",
|
||||
"operator-channel.asked.claude-code",
|
||||
"operator-channel.asked.mesh-config",
|
||||
"operator-channel.asked.mesh-delivery-assigner",
|
||||
"operator-channel.asked.mesh-delivery-teller",
|
||||
"operator-channel.asked.mesh-grants",
|
||||
"operator-channel.asked.mesh-knowledge",
|
||||
"operator-channel.asked.mesh-mail",
|
||||
"operator-channel.asked.pacman"
|
||||
"operator-channel.decided.claude-code"
|
||||
],
|
||||
"provides": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user