refactor: integrate chat persistence into Room
Move SaveMessage/LoadMessages logic into Room as private methods. NewPersistentRoom auto-loads history and auto-saves on Send, removing the need for handlers to coordinate persistence separately.
This commit is contained in:
73
chat/chat.go
73
chat/chat.go
@@ -3,11 +3,15 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/ryanhamamura/games/db/repository"
|
||||
)
|
||||
|
||||
// Message is the wire format for chat messages over NATS.
|
||||
@@ -21,26 +25,47 @@ type Message struct {
|
||||
const maxMessages = 50
|
||||
|
||||
// Room manages an in-memory message buffer and NATS pub/sub for a single
|
||||
// chat room (typically one per game).
|
||||
// chat room (typically one per game). When created with NewPersistentRoom,
|
||||
// messages are automatically loaded from and saved to the database.
|
||||
type Room struct {
|
||||
subject string
|
||||
nc *nats.Conn
|
||||
messages []Message
|
||||
mu sync.Mutex
|
||||
|
||||
// Optional persistence; nil for ephemeral rooms (e.g. snake).
|
||||
queries *repository.Queries
|
||||
roomID string
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// NewRoom creates an ephemeral chat room with no database persistence.
|
||||
func NewRoom(nc *nats.Conn, subject string) *Room {
|
||||
return &Room{
|
||||
subject: subject,
|
||||
nc: nc,
|
||||
messages: initial,
|
||||
subject: subject,
|
||||
nc: nc,
|
||||
}
|
||||
}
|
||||
|
||||
// Send publishes a message to the room's NATS subject.
|
||||
// NewPersistentRoom creates a chat room backed by the database. It loads
|
||||
// existing messages on creation and auto-saves new messages on Send.
|
||||
func NewPersistentRoom(nc *nats.Conn, subject string, queries *repository.Queries, roomID string) *Room {
|
||||
r := &Room{
|
||||
subject: subject,
|
||||
nc: nc,
|
||||
queries: queries,
|
||||
roomID: roomID,
|
||||
}
|
||||
r.messages = r.loadMessages()
|
||||
return r
|
||||
}
|
||||
|
||||
// Send publishes a message to the room's NATS subject and persists it
|
||||
// if the room is backed by a database.
|
||||
func (r *Room) Send(msg Message) {
|
||||
if r.queries != nil {
|
||||
r.saveMessage(msg)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("subject", r.subject).Msg("failed to marshal chat message")
|
||||
@@ -90,3 +115,35 @@ func (r *Room) Subscribe() (chan *nats.Msg, *nats.Subscription, error) {
|
||||
}
|
||||
return ch, sub, nil
|
||||
}
|
||||
|
||||
func (r *Room) saveMessage(msg Message) {
|
||||
err := r.queries.CreateChatMessage(context.Background(), repository.CreateChatMessageParams{
|
||||
GameID: r.roomID,
|
||||
Nickname: msg.Nickname,
|
||||
Color: int64(msg.Slot),
|
||||
Message: msg.Message,
|
||||
CreatedAt: msg.Time,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("room_id", r.roomID).Msg("failed to save chat message")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Room) loadMessages() []Message {
|
||||
rows, err := r.queries.GetChatMessages(context.Background(), r.roomID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
msgs := make([]Message, len(rows))
|
||||
for i, row := range rows {
|
||||
msgs[i] = Message{
|
||||
Nickname: row.Nickname,
|
||||
Slot: int(row.Color),
|
||||
Message: row.Message,
|
||||
Time: row.CreatedAt,
|
||||
}
|
||||
}
|
||||
// DB returns newest-first; reverse for chronological display
|
||||
slices.Reverse(msgs)
|
||||
return msgs
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user