188 lines
4.7 KiB
Go
188 lines
4.7 KiB
Go
package memory
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"telesrv/internal/domain"
|
|
"telesrv/internal/store"
|
|
)
|
|
|
|
// BroadcastStore is the in-memory implementation of store.BroadcastStore,
|
|
// used by admin/app unit tests.
|
|
type BroadcastStore struct {
|
|
mu sync.Mutex
|
|
broadcasts map[int64]domain.Broadcast
|
|
recipients map[int64]*memBroadcastRecipient
|
|
nextBID int64
|
|
nextRID int64
|
|
}
|
|
|
|
type memBroadcastRecipient struct {
|
|
domain.BroadcastRecipient
|
|
message string
|
|
}
|
|
|
|
func NewBroadcastStore() *BroadcastStore {
|
|
return &BroadcastStore{
|
|
broadcasts: make(map[int64]domain.Broadcast),
|
|
recipients: make(map[int64]*memBroadcastRecipient),
|
|
}
|
|
}
|
|
|
|
var _ store.BroadcastStore = (*BroadcastStore)(nil)
|
|
|
|
func (s *BroadcastStore) CreateBroadcast(_ context.Context, message string, targetMode domain.BroadcastTargetMode, recipientUserIDs []int64, createdBy string) (domain.Broadcast, error) {
|
|
if len(recipientUserIDs) == 0 {
|
|
return domain.Broadcast{}, domain.ErrBroadcastNoRecipients
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.nextBID++
|
|
b := domain.Broadcast{
|
|
ID: s.nextBID,
|
|
Message: message,
|
|
TargetMode: targetMode,
|
|
CreatedBy: createdBy,
|
|
CreatedAt: time.Now().UTC(),
|
|
}
|
|
seen := make(map[int64]bool, len(recipientUserIDs))
|
|
for _, userID := range recipientUserIDs {
|
|
if seen[userID] {
|
|
continue
|
|
}
|
|
seen[userID] = true
|
|
s.nextRID++
|
|
s.recipients[s.nextRID] = &memBroadcastRecipient{
|
|
BroadcastRecipient: domain.BroadcastRecipient{
|
|
ID: s.nextRID,
|
|
BroadcastID: b.ID,
|
|
UserID: userID,
|
|
Status: domain.BroadcastRecipientPending,
|
|
},
|
|
message: message,
|
|
}
|
|
b.TotalCount++
|
|
}
|
|
s.broadcasts[b.ID] = b
|
|
return b, nil
|
|
}
|
|
|
|
func (s *BroadcastStore) PendingBroadcastRecipients(_ context.Context, limit int) ([]store.PendingBroadcastRecipient, error) {
|
|
if limit <= 0 || limit > 200 {
|
|
limit = 50
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
// Iteration order over a map is unspecified; sort by recipient id (assigned
|
|
// in creation order) so this matches the postgres backend's "oldest first".
|
|
ids := make([]int64, 0, len(s.recipients))
|
|
for id, r := range s.recipients {
|
|
if r.Status == domain.BroadcastRecipientPending {
|
|
ids = append(ids, id)
|
|
}
|
|
}
|
|
sortInt64s(ids)
|
|
out := make([]store.PendingBroadcastRecipient, 0, limit)
|
|
for _, id := range ids {
|
|
if len(out) >= limit {
|
|
break
|
|
}
|
|
r := s.recipients[id]
|
|
out = append(out, store.PendingBroadcastRecipient{
|
|
RecipientID: r.ID, BroadcastID: r.BroadcastID, UserID: r.UserID, Attempts: r.Attempts, Message: r.message,
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *BroadcastStore) MarkBroadcastRecipientSent(_ context.Context, recipientID int64) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
r, ok := s.recipients[recipientID]
|
|
if !ok || r.Status != domain.BroadcastRecipientPending {
|
|
return nil
|
|
}
|
|
r.Status = domain.BroadcastRecipientSent
|
|
now := time.Now().UTC()
|
|
r.SentAt = &now
|
|
r.LastError = ""
|
|
return nil
|
|
}
|
|
|
|
func (s *BroadcastStore) MarkBroadcastRecipientFailed(_ context.Context, recipientID int64, reason string) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
r, ok := s.recipients[recipientID]
|
|
if !ok || r.Status != domain.BroadcastRecipientPending {
|
|
return nil
|
|
}
|
|
r.Attempts++
|
|
r.LastError = reason
|
|
if r.Attempts >= domain.MaxBroadcastRecipientAttempts {
|
|
r.Status = domain.BroadcastRecipientFailed
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *BroadcastStore) countsFor(broadcastID int64) (sent, failed int) {
|
|
for _, r := range s.recipients {
|
|
if r.BroadcastID != broadcastID {
|
|
continue
|
|
}
|
|
switch r.Status {
|
|
case domain.BroadcastRecipientSent:
|
|
sent++
|
|
case domain.BroadcastRecipientFailed:
|
|
failed++
|
|
}
|
|
}
|
|
return sent, failed
|
|
}
|
|
|
|
func (s *BroadcastStore) ListBroadcasts(_ context.Context, beforeID int64, limit int) ([]domain.Broadcast, bool, error) {
|
|
if limit <= 0 || limit > 200 {
|
|
limit = 50
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
ids := make([]int64, 0, len(s.broadcasts))
|
|
for id := range s.broadcasts {
|
|
if beforeID == 0 || id < beforeID {
|
|
ids = append(ids, id)
|
|
}
|
|
}
|
|
sortInt64sDesc(ids)
|
|
hasMore := len(ids) > limit
|
|
if hasMore {
|
|
ids = ids[:limit]
|
|
}
|
|
out := make([]domain.Broadcast, 0, len(ids))
|
|
for _, id := range ids {
|
|
b := s.broadcasts[id]
|
|
b.SentCount, b.FailedCount = s.countsFor(id)
|
|
out = append(out, b)
|
|
}
|
|
return out, hasMore, nil
|
|
}
|
|
|
|
func (s *BroadcastStore) BroadcastByID(_ context.Context, id int64) (domain.Broadcast, bool, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
b, ok := s.broadcasts[id]
|
|
if !ok {
|
|
return domain.Broadcast{}, false, nil
|
|
}
|
|
b.SentCount, b.FailedCount = s.countsFor(id)
|
|
return b, true, nil
|
|
}
|
|
|
|
func sortInt64s(v []int64) {
|
|
sort.Slice(v, func(i, j int) bool { return v[i] < v[j] })
|
|
}
|
|
|
|
func sortInt64sDesc(v []int64) {
|
|
sort.Slice(v, func(i, j int) bool { return v[i] > v[j] })
|
|
}
|