From 7dd63bb7f91bc74e34ba5cdd02ba8d5a03f2af27 Mon Sep 17 00:00:00 2001
From: pdewey
Date: Sun, 11 Jan 2026 21:24:10 -0500
Subject: [PATCH] feat: cache and limit feed fetches for unauthenticated users
---
BACKLOG.md | 22 ++++++++++++
internal/feed/service.go | 68 +++++++++++++++++++++++++++++++++++
internal/handlers/handlers.go | 13 ++++++-
3 files changed, 102 insertions(+), 1 deletion(-)
create mode 100644 BACKLOG.md
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 {
--
2.51.2