Coffee journaling on ATProto (alpha) alpha.arabica.social
coffee

feat: cache and limit feed fetches for unauthenticated users

pdewey.com 7dd63bb7 55bc6ac6

verified
+102 -1
+22
BACKLOG.md
··· 1 + ## Description 2 + 3 + This file includes the backlog of features and fixes that need to be done. 4 + Each should be addressed one at a time, and the item should be removed after implementation has been finished and verified. 5 + 6 + --- 7 + 8 + ## Features 9 + 10 + 1. LARGE: complete record styling refactor that changes from table-style to more mobile-friendly style 11 + - Likely a more "post-style" version that is closer to bsky posts 12 + - To be done later down the line 13 + - setting to use legacy table view 14 + 15 + 2. Settings menu (mostly tbd) 16 + - Private mode -- don't show in community feed (records are still public via pds api though) 17 + - Dev mode -- show did, copy did in profiles (remove "logged in as <did>" from home page) 18 + - Toggle for table view vs future post-style view 19 + 20 + ## Fixes 21 + 22 + - Loading columns for brews table doesn't match loaded column names
+68
internal/feed/service.go
··· 13 13 "github.com/rs/zerolog/log" 14 14 ) 15 15 16 + // PublicFeedCacheTTL is the duration for which the public feed cache is valid. 17 + // This value can be adjusted based on desired freshness vs. performance tradeoff. 18 + // Consider values between 5-10 minutes for a good balance. 19 + const PublicFeedCacheTTL = 5 * time.Minute 20 + 21 + // PublicFeedLimit is the number of items to show for unauthenticated users 22 + const PublicFeedLimit = 5 23 + 16 24 // FeedItem represents an activity in the social feed with author info 17 25 type FeedItem struct { 18 26 // Record type and data (only one will be non-nil) ··· 30 38 TimeAgo string // "2 hours ago", "yesterday", etc. 31 39 } 32 40 41 + // publicFeedCache holds cached feed items for unauthenticated users 42 + type publicFeedCache struct { 43 + items []*FeedItem 44 + expiresAt time.Time 45 + mu sync.RWMutex 46 + } 47 + 33 48 // Service fetches and aggregates brews from registered users 34 49 type Service struct { 35 50 registry *Registry 36 51 publicClient *atproto.PublicClient 52 + cache *publicFeedCache 37 53 } 38 54 39 55 // NewService creates a new feed service ··· 41 57 return &Service{ 42 58 registry: registry, 43 59 publicClient: atproto.NewPublicClient(), 60 + cache: &publicFeedCache{}, 44 61 } 62 + } 63 + 64 + // GetCachedPublicFeed returns cached feed items for unauthenticated users. 65 + // It returns up to PublicFeedLimit items from the cache, refreshing if expired. 66 + func (s *Service) GetCachedPublicFeed(ctx context.Context) ([]*FeedItem, error) { 67 + s.cache.mu.RLock() 68 + if time.Now().Before(s.cache.expiresAt) && len(s.cache.items) > 0 { 69 + items := s.cache.items 70 + s.cache.mu.RUnlock() 71 + log.Debug().Int("item_count", len(items)).Msg("feed: returning cached public feed") 72 + return items, nil 73 + } 74 + s.cache.mu.RUnlock() 75 + 76 + // Cache is expired or empty, refresh it 77 + return s.refreshPublicFeedCache(ctx) 78 + } 79 + 80 + // refreshPublicFeedCache fetches fresh feed items and updates the cache 81 + func (s *Service) refreshPublicFeedCache(ctx context.Context) ([]*FeedItem, error) { 82 + s.cache.mu.Lock() 83 + defer s.cache.mu.Unlock() 84 + 85 + // Double-check if another goroutine already refreshed the cache 86 + if time.Now().Before(s.cache.expiresAt) && len(s.cache.items) > 0 { 87 + return s.cache.items, nil 88 + } 89 + 90 + log.Debug().Msg("feed: refreshing public feed cache") 91 + 92 + // Fetch fresh feed items (limited to PublicFeedLimit) 93 + items, err := s.GetRecentRecords(ctx, PublicFeedLimit) 94 + if err != nil { 95 + // If we have stale data, return it rather than failing 96 + if len(s.cache.items) > 0 { 97 + log.Warn().Err(err).Msg("feed: failed to refresh cache, returning stale data") 98 + return s.cache.items, nil 99 + } 100 + return nil, err 101 + } 102 + 103 + // Update cache 104 + s.cache.items = items 105 + s.cache.expiresAt = time.Now().Add(PublicFeedCacheTTL) 106 + 107 + log.Debug(). 108 + Int("item_count", len(items)). 109 + Time("expires_at", s.cache.expiresAt). 110 + Msg("feed: updated public feed cache") 111 + 112 + return items, nil 45 113 } 46 114 47 115 // GetRecentRecords fetches recent activity (brews and other records) from all registered users
+12 -1
internal/handlers/handlers.go
··· 156 156 // Community feed partial (loaded async via HTMX) 157 157 func (h *Handler) HandleFeedPartial(w http.ResponseWriter, r *http.Request) { 158 158 var feedItems []*feed.FeedItem 159 + 159 160 if h.feedService != nil { 160 - feedItems, _ = h.feedService.GetRecentRecords(r.Context(), 20) 161 + // Check if user is authenticated 162 + _, err := atproto.GetAuthenticatedDID(r.Context()) 163 + isAuthenticated := err == nil 164 + 165 + if isAuthenticated { 166 + // Authenticated users get the full feed (20 items), fetched fresh 167 + feedItems, _ = h.feedService.GetRecentRecords(r.Context(), 20) 168 + } else { 169 + // Unauthenticated users get a limited feed from the cache 170 + feedItems, _ = h.feedService.GetCachedPublicFeed(r.Context()) 171 + } 161 172 } 162 173 163 174 if err := bff.RenderFeedPartial(w, feedItems); err != nil {