package link import ( "context" "fmt" "time" "github.com/nats-io/nats.go" "github.com/novox/mesh-controller/internal/broker" ) // OverNats is the controller's outbound on the bus being built: the same three acts the other // transport has, on the subjects the permissions were derived for (design 25). A declaration is a // JetStream publish into NODES, where the node's own consumer waits for it; an event is announced on // the subject its name derives to; a tool is asked by request and reply on the module's tool subject. type OverNats struct{ JS *broker.JetStream } // declareSubject is where one node's declaration lands — the NODES stream's subject for it, and the // only subject that node's consumer delivers. The host subscribes exactly this. func declareSubject(node string) string { return "mesh.node." + node + ".declare" } func (b OverNats) PublishDeclaration(ctx context.Context, node string, body []byte) error { publish, cancel := context.WithTimeout(ctx, 15*time.Second) defer cancel() if _, err := b.JS.Context().Publish(declareSubject(node), body, nats.Context(publish)); err != nil { return fmt.Errorf("declaring to %s: %w", node, err) } return nil } func (b OverNats) PublishEvent(ctx context.Context, key, source, node string, body []byte) error { // The key is the subject: the controller's own events are named in full, and what a module // emits is derived before it reaches here. Headers carry the envelope the other transport put // in message properties (ADR 0042), so a consumer reads who and when without the payload. msg := nats.NewMsg(key) msg.Data = body msg.Header.Set("x-source", source) msg.Header.Set("x-node", node) msg.Header.Set("x-time", time.Now().UTC().Format(time.RFC3339Nano)) if err := b.JS.Conn().PublishMsg(msg); err != nil { return fmt.Errorf("announcing %s: %w", key, err) } return nil } func (b OverNats) AskTool(ctx context.Context, module, tool string, args []byte, timeout time.Duration) ([]byte, error) { ask, cancel := context.WithTimeout(ctx, timeout) defer cancel() reply, err := b.JS.Conn().RequestWithContext(ask, "mesh.mod."+module+".tool."+tool, args) if err != nil { return nil, fmt.Errorf("asking %s.%s: %w", module, tool, err) } return reply.Data, nil } // ConnectNats is Connect for the bus being built: the controller's inbound and outbound over one // JetStream connection the caller has already raised the streams on. Nothing is declared here — // the streams and the controller's consumers are asserted by Raise, before anything is served. func ConnectNats(js *broker.JetStream, enroller Enroller, listener Listener) *Server { return &Server{ inbound: Nats(js), bus: OverNats{JS: js}, js: js, enroller: enroller, listener: listener, log: newLog(), } } // Bus is the controller's outbound, whichever transport it connected over. Callers that send a // declaration or ask a tool use this rather than the channel, which one transport does not have. func (s *Server) Bus() Bus { return s.bus }