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 }