// Package chat provides a reusable chat room backed by NATS pub/sub // with optional database persistence. package chat import ( "encoding/json" "sync" "github.com/nats-io/nats.go" "github.com/rs/zerolog/log" ) // Message is the wire format for chat messages over NATS. type Message struct { Nickname string `json:"nickname"` Slot int `json:"slot"` // player slot/color index Message string `json:"message"` Time int64 `json:"time"` // unix millis, zero for ephemeral messages } const maxMessages = 50 // Room manages an in-memory message buffer and NATS pub/sub for a single // chat room (typically one per game). type Room struct { subject string nc *nats.Conn messages []Message mu sync.Mutex } // NewRoom creates a chat room that publishes and subscribes on the given // NATS subject (e.g. "chat.abc123"). func NewRoom(nc *nats.Conn, subject string, initial []Message) *Room { return &Room{ subject: subject, nc: nc, messages: initial, } } // Send publishes a message to the room's NATS subject. func (r *Room) Send(msg Message) { data, err := json.Marshal(msg) if err != nil { log.Error().Err(err).Str("subject", r.subject).Msg("failed to marshal chat message") return } if err := r.nc.Publish(r.subject, data); err != nil { log.Error().Err(err).Str("subject", r.subject).Msg("failed to publish chat message") } } // Receive processes an incoming NATS message, appending it to the buffer. // Returns the new message and a snapshot of all messages. func (r *Room) Receive(data []byte) (Message, []Message) { var msg Message if err := json.Unmarshal(data, &msg); err != nil { return msg, nil } r.mu.Lock() r.messages = append(r.messages, msg) if len(r.messages) > maxMessages { r.messages = r.messages[len(r.messages)-maxMessages:] } snapshot := make([]Message, len(r.messages)) copy(snapshot, r.messages) r.mu.Unlock() return msg, snapshot } // Messages returns a snapshot of the current message buffer. func (r *Room) Messages() []Message { r.mu.Lock() defer r.mu.Unlock() snapshot := make([]Message, len(r.messages)) copy(snapshot, r.messages) return snapshot } // Subscribe creates a NATS channel subscription for the room's subject. // Caller is responsible for unsubscribing. func (r *Room) Subscribe() (chan *nats.Msg, *nats.Subscription, error) { ch := make(chan *nats.Msg, 64) sub, err := r.nc.ChanSubscribe(r.subject, ch) if err != nil { return nil, nil, err } return ch, sub, nil }