- internal/nats: Connection interface + InProcessConn, 7 tests - internal/db: PostgreSQL connection pool + migration runner, 5 tests - 75 total tests, build + vet clean
156 lines
4.0 KiB
Go
156 lines
4.0 KiB
Go
// 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)
|