diff --git a/BACKLOG.md b/BACKLOG.md new file mode 100644 index 0000000..e781240 --- /dev/null +++ b/BACKLOG.md @@ -0,0 +1,22 @@ +## Description + +This file includes the backlog of features and fixes that need to be done. +Each should be addressed one at a time, and the item should be removed after implementation has been finished and verified. + +--- + +## Features + +1. LARGE: complete record styling refactor that changes from table-style to more mobile-friendly style + - Likely a more "post-style" version that is closer to bsky posts + - To be done later down the line + - setting to use legacy table view + +2. Settings menu (mostly tbd) + - Private mode -- don't show in community feed (records are still public via pds api though) + - Dev mode -- show did, copy did in profiles (remove "logged in as " from home page) + - Toggle for table view vs future post-style view + +## Fixes + +- Loading columns for brews table doesn't match loaded column names diff --git a/internal/feed/service.go b/internal/feed/service.go index 7f42e11..f716612 100644 --- a/internal/feed/service.go +++ b/internal/feed/service.go @@ -13,6 +13,14 @@ import ( "github.com/rs/zerolog/log" ) +// PublicFeedCacheTTL is the duration for which the public feed cache is valid. +// This value can be adjusted based on desired freshness vs. performance tradeoff. +// Consider values between 5-10 minutes for a good balance. +const PublicFeedCacheTTL = 5 * time.Minute + +// PublicFeedLimit is the number of items to show for unauthenticated users +const PublicFeedLimit = 5 + // FeedItem represents an activity in the social feed with author info type FeedItem struct { // Record type and data (only one will be non-nil) @@ -30,10 +38,18 @@ type FeedItem struct { TimeAgo string // "2 hours ago", "yesterday", etc. } +// publicFeedCache holds cached feed items for unauthenticated users +type publicFeedCache struct { + items []*FeedItem + expiresAt time.Time + mu sync.RWMutex +} + // Service fetches and aggregates brews from registered users type Service struct { registry *Registry publicClient *atproto.PublicClient + cache *publicFeedCache } // NewService creates a new feed service @@ -41,7 +57,59 @@ func NewService(registry *Registry) *Service { return &Service{ registry: registry, publicClient: atproto.NewPublicClient(), + cache: &publicFeedCache{}, + } +} + +// GetCachedPublicFeed returns cached feed items for unauthenticated users. +// It returns up to PublicFeedLimit items from the cache, refreshing if expired. +func (s *Service) GetCachedPublicFeed(ctx context.Context) ([]*FeedItem, error) { + s.cache.mu.RLock() + if time.Now().Before(s.cache.expiresAt) && len(s.cache.items) > 0 { + items := s.cache.items + s.cache.mu.RUnlock() + log.Debug().Int("item_count", len(items)).Msg("feed: returning cached public feed") + return items, nil + } + s.cache.mu.RUnlock() + + // Cache is expired or empty, refresh it + return s.refreshPublicFeedCache(ctx) +} + +// refreshPublicFeedCache fetches fresh feed items and updates the cache +func (s *Service) refreshPublicFeedCache(ctx context.Context) ([]*FeedItem, error) { + s.cache.mu.Lock() + defer s.cache.mu.Unlock() + + // Double-check if another goroutine already refreshed the cache + if time.Now().Before(s.cache.expiresAt) && len(s.cache.items) > 0 { + return s.cache.items, nil + } + + log.Debug().Msg("feed: refreshing public feed cache") + + // Fetch fresh feed items (limited to PublicFeedLimit) + items, err := s.GetRecentRecords(ctx, PublicFeedLimit) + if err != nil { + // If we have stale data, return it rather than failing + if len(s.cache.items) > 0 { + log.Warn().Err(err).Msg("feed: failed to refresh cache, returning stale data") + return s.cache.items, nil + } + return nil, err } + + // Update cache + s.cache.items = items + s.cache.expiresAt = time.Now().Add(PublicFeedCacheTTL) + + log.Debug(). + Int("item_count", len(items)). + Time("expires_at", s.cache.expiresAt). + Msg("feed: updated public feed cache") + + return items, nil } // GetRecentRecords fetches recent activity (brews and other records) from all registered users diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index fa3b7e7..5db2b89 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -156,8 +156,19 @@ func (h *Handler) HandleHome(w http.ResponseWriter, r *http.Request) { // Community feed partial (loaded async via HTMX) func (h *Handler) HandleFeedPartial(w http.ResponseWriter, r *http.Request) { var feedItems []*feed.FeedItem + if h.feedService != nil { - feedItems, _ = h.feedService.GetRecentRecords(r.Context(), 20) + // Check if user is authenticated + _, err := atproto.GetAuthenticatedDID(r.Context()) + isAuthenticated := err == nil + + if isAuthenticated { + // Authenticated users get the full feed (20 items), fetched fresh + feedItems, _ = h.feedService.GetRecentRecords(r.Context(), 20) + } else { + // Unauthenticated users get a limited feed from the cache + feedItems, _ = h.feedService.GetCachedPublicFeed(r.Context()) + } } if err := bff.RenderFeedPartial(w, feedItems); err != nil {