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

Configure Feed

Select the types of activity you want to include in your feed.

feat: profile picture caching to reduce public api calls

authored by

Patrick Dewey and committed by tangled.org f8f324a7 33fecd97

+36
+36
internal/handlers/handlers.go
··· 8 8 "net/http" 9 9 "strconv" 10 10 "strings" 11 + "sync" 12 + "time" 11 13 12 14 "arabica/internal/atproto" 13 15 "arabica/internal/database" ··· 25 27 "github.com/rs/zerolog/log" 26 28 ) 27 29 30 + // profileCacheTTL controls how long user profiles are cached before re-fetching. 31 + // Profiles (avatar, display name) change infrequently so 1 hour is reasonable. 32 + const profileCacheTTL = 1 * time.Hour 33 + 34 + // cachedProfile holds a user profile with its fetch timestamp. 35 + type cachedProfile struct { 36 + profile *bff.UserProfile 37 + cachedAt time.Time 38 + } 39 + 28 40 // Config holds handler configuration options 29 41 type Config struct { 30 42 // SecureCookies sets the Secure flag on authentication cookies ··· 57 69 joinStore *boltstore.JoinStore 58 70 pdsAdminURL string 59 71 pdsAdminToken string 72 + 73 + // profileCache caches user profiles (avatar, handle) by DID to avoid 74 + // hitting the Bluesky API on every page load. 75 + profileCache map[string]*cachedProfile 76 + profileCacheMu sync.RWMutex 60 77 } 61 78 62 79 // NewHandler creates a new Handler with all required dependencies. ··· 76 93 config: config, 77 94 feedService: feedService, 78 95 feedRegistry: feedRegistry, 96 + profileCache: make(map[string]*cachedProfile), 79 97 } 80 98 } 81 99 ··· 184 202 } 185 203 186 204 // getUserProfile fetches the profile for an authenticated user. 205 + // Results are cached by DID for profileCacheTTL to avoid hitting the 206 + // Bluesky API on every page load. 187 207 // Returns nil if unable to fetch profile (non-fatal error). 188 208 func (h *Handler) getUserProfile(ctx context.Context, did string) *bff.UserProfile { 189 209 if did == "" { 190 210 return nil 191 211 } 192 212 213 + // Check cache 214 + h.profileCacheMu.RLock() 215 + if cached, ok := h.profileCache[did]; ok && time.Since(cached.cachedAt) < profileCacheTTL { 216 + h.profileCacheMu.RUnlock() 217 + return cached.profile 218 + } 219 + h.profileCacheMu.RUnlock() 220 + 193 221 publicClient := atproto.NewPublicClient() 194 222 profile, err := publicClient.GetProfile(ctx, did) 195 223 if err != nil { ··· 206 234 if profile.Avatar != nil { 207 235 userProfile.Avatar = *profile.Avatar 208 236 } 237 + 238 + // Store in cache 239 + h.profileCacheMu.Lock() 240 + h.profileCache[did] = &cachedProfile{ 241 + profile: userProfile, 242 + cachedAt: time.Now(), 243 + } 244 + h.profileCacheMu.Unlock() 209 245 210 246 return userProfile 211 247 }