fluid-telegram/internal/adapter/store.go

98 lines
2.4 KiB
Go
Raw Normal View History

package adapter
import (
"sort"
"sync"
"time"
)
// State is a publication's lifecycle, per contracts/r1.openapi.yaml's `state`
// enum on listPosts.
type State string
const (
StatePending State = "pending"
StatePublished State = "published"
StateFailed State = "failed"
StateSuperseded State = "superseded"
)
// Publication is this adapter's own record of a post_id: which Telegram
// message implements it. The evidence store records that a publication
// happened; this is the adapter's mapping, kept separately per
// docs/adapter-contract.md.
type Publication struct {
PostID string
State State
TelegramMsgID int
Body string // the rendered text or caption last sent, for edit-detection
SourceEntryID string
Variant string
PublishedAt time.Time
LastAttemptErr string
}
// Store is the adapter's idempotency mapping from post_id to what was sent.
// It is the adapter's own state, never fluid-core's.
type Store interface {
Get(postID string) (Publication, bool)
Put(Publication)
List(filter ListFilter) []Publication
}
// ListFilter narrows GET /v1/channel-posts, mirroring its query parameters.
type ListFilter struct {
State State
Variant string
SourceEntryID string
Limit int
}
type memStore struct {
mu sync.RWMutex
byID map[string]Publication
}
// NewMemStore is an in-process idempotency store. It is what one adapter
// instance needs for one channel; nothing here is shared with fluid-core or
// with another channel's adapter instance.
func NewMemStore() Store {
return &memStore{byID: map[string]Publication{}}
}
func (s *memStore) Get(postID string) (Publication, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
p, ok := s.byID[postID]
return p, ok
}
func (s *memStore) Put(p Publication) {
s.mu.Lock()
defer s.mu.Unlock()
s.byID[p.PostID] = p
}
func (s *memStore) List(f ListFilter) []Publication {
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]Publication, 0, len(s.byID))
for _, p := range s.byID {
if f.State != "" && p.State != f.State {
continue
}
if f.Variant != "" && p.Variant != f.Variant {
continue
}
if f.SourceEntryID != "" && p.SourceEntryID != f.SourceEntryID {
continue
}
out = append(out, p)
}
sort.Slice(out, func(i, j int) bool { return out[i].PublishedAt.After(out[j].PublishedAt) })
if f.Limit > 0 && len(out) > f.Limit {
out = out[:f.Limit]
}
return out
}