// Package nats provides an abstraction layer for NATS messaging. // It defines interfaces for pub/sub and request-reply patterns, // allowing the real nats.go client or in-process mocks to be used interchangeably. package nats import ( "context" "time" ) // Connection is the core abstraction for NATS communication. // Backed by a real NATS connection in production, or an in-process stub in development. type Connection interface { // Publish sends a message to the given subject asynchronously. Publish(subject string, data []byte) error // Request sends a request and waits for a reply with the given timeout. Request(subject string, data []byte, timeout time.Duration) (*Message, error) // Subscribe subscribes to a subject and returns a message channel. Subscribe(subject string) (*Subscription, error) // IsConnected returns whether the connection is active. IsConnected() bool // Close shuts down the connection. Close() error } // Message represents a received NATS message. type Message struct { Subject string Data []byte Reply string } // Subscription wraps a NATS subscription. type Subscription struct { Subject string MsgCh <-chan *Message unsub func() error } // Unsubscribe removes the subscription. func (s *Subscription) Unsubscribe() error { if s.unsub != nil { return s.unsub() } return nil } // InProcessConn is a simple in-process NATS connection for development and testing. // It routes messages between publishers and subscribers within the same process. type InProcessConn struct { subs map[string][]chan *Message done chan struct{} } // NewInProcess creates a new in-process NATS connection. func NewInProcess() *InProcessConn { return &InProcessConn{ subs: make(map[string][]chan *Message), done: make(chan struct{}), } } func (c *InProcessConn) Publish(subject string, data []byte) error { msg := &Message{Subject: subject, Data: data} for _, ch := range c.subs[subject] { select { case ch <- msg: default: // Drop if subscriber is too slow } } return nil } func (c *InProcessConn) Request(subject string, data []byte, timeout time.Duration) (*Message, error) { // In-process request: publish and wait for first reply on a temp reply subject replySubj := subject + ".reply." + randomSuffix() replyCh := make(chan *Message, 1) c.subs[replySubj] = append(c.subs[replySubj], replyCh) // Publish with reply subject msg := &Message{Subject: subject, Data: data, Reply: replySubj} for _, ch := range c.subs[subject] { select { case ch <- msg: default: } } ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() select { case reply := <-replyCh: return reply, nil case <-ctx.Done(): return nil, ctx.Err() } } func (c *InProcessConn) Subscribe(subject string) (*Subscription, error) { ch := make(chan *Message, 64) c.subs[subject] = append(c.subs[subject], ch) return &Subscription{ Subject: subject, MsgCh: ch, unsub: func() error { // Remove this channel from subs (best effort) return nil }, }, nil } func (c *InProcessConn) IsConnected() bool { return true } func (c *InProcessConn) Close() error { close(c.done) return nil } func randomSuffix() string { return time.Now().Format("150405.000000") } // Subject constants — all Cyrene subjects follow the "cyrene.{service}.{action}" convention. const ( SubjectPrefix = "cyrene" SubjMemorySearch = "cyrene.memory.search" SubjMemoryInsert = "cyrene.memory.insert" SubjMemoryDelete = "cyrene.memory.delete" SubjMemoryExtract = "cyrene.memory.extract" SubjVoiceSTT = "cyrene.voice.stt" SubjVoiceTTS = "cyrene.voice.tts" SubjIoTDeviceList = "cyrene.iot.devices.list" SubjIoTDeviceToggle = "cyrene.iot.device.toggle" SubjIoTStateChanged = "cyrene.iot.state.changed" SubjBridgeIncoming = "cyrene.bridge.incoming" SubjBridgeOutgoing = "cyrene.bridge.outgoing" SubjThinkerThoughtReady = "cyrene.thinker.thought.ready" SubjRAGSearch = "cyrene.rag.search" ) // Ensure InProcessConn satisfies Connection var _ Connection = (*InProcessConn)(nil)