A third party ATProto appview

pfp cache fix

Changed files
+66 -10
server
+15
server/routes.ts
··· 4394 4394 } 4395 4395 }); 4396 4396 4397 + app.post('/api/cache/clear-hydration', async (_req, res) => { 4398 + try { 4399 + const { optimizedHydrator } = await import('./services/hydration/optimized-hydrator'); 4400 + await optimizedHydrator.clearCache(); 4401 + 4402 + res.json({ 4403 + success: true, 4404 + message: 'Hydration cache cleared successfully' 4405 + }); 4406 + } catch (err) { 4407 + console.error('[CACHE] Error clearing hydration cache:', err); 4408 + res.status(500).json({ error: 'InternalServerError', message: 'Failed to clear hydration cache' }); 4409 + } 4410 + }); 4411 + 4397 4412 // Minimal dead-letter inspection endpoint (admin only in production) 4398 4413 app.get('/api/redis/dead-letters', async (_req, res) => { 4399 4414 try {
+46 -1
server/services/hydration/cache.ts
··· 1 1 import { CacheService } from '../cache'; 2 2 3 3 export class HydrationCache { 4 - private readonly TTL = 1800; // 30 minutes (increased for small userbase) 4 + private readonly TTL = 300; // 5 minutes (reduced to avoid stale profile data) 5 5 private cache: CacheService; 6 6 7 7 constructor() { ··· 169 169 */ 170 170 labelsKey(uri: string): string { 171 171 return `labels:${uri}`; 172 + } 173 + 174 + /** 175 + * Clear all hydration cache (useful after profile updates or new installs) 176 + */ 177 + async clearAll(): Promise<void> { 178 + const cacheService = this.cache as any; 179 + if (!cacheService.redis || !cacheService.isInitialized) { 180 + console.warn('[HYDRATION_CACHE] Redis not available, cannot clear cache'); 181 + return; 182 + } 183 + 184 + try { 185 + // Use SCAN to find all hydration keys and delete them 186 + const pattern = 'hydration:*'; 187 + const keys: string[] = []; 188 + let cursor = '0'; 189 + 190 + do { 191 + const result = await cacheService.redis.scan( 192 + cursor, 193 + 'MATCH', 194 + pattern, 195 + 'COUNT', 196 + 100 197 + ); 198 + cursor = result[0]; 199 + keys.push(...result[1]); 200 + } while (cursor !== '0'); 201 + 202 + if (keys.length > 0) { 203 + console.log(`[HYDRATION_CACHE] Clearing ${keys.length} cached items`); 204 + // Delete in batches of 100 205 + for (let i = 0; i < keys.length; i += 100) { 206 + const batch = keys.slice(i, i + 100); 207 + await cacheService.redis.del(...batch); 208 + } 209 + console.log('[HYDRATION_CACHE] Cache cleared successfully'); 210 + } else { 211 + console.log('[HYDRATION_CACHE] No cached items to clear'); 212 + } 213 + } catch (error) { 214 + console.error('[HYDRATION_CACHE] Error clearing cache:', error); 215 + throw error; 216 + } 172 217 } 173 218 }
+5 -9
server/services/hydration/optimized-hydrator.ts
··· 724 724 this.embedResolver.clearCache(); 725 725 this.requestCache.clear(); 726 726 727 - if (this.redis) { 728 - try { 729 - const keys = await this.redis.keys('hydration:*'); 730 - if (keys.length > 0) { 731 - await this.redis.del(...keys); 732 - } 733 - } catch (error) { 734 - console.error('[OPTIMIZED_HYDRATION] Cache clear error:', error); 735 - } 727 + // Use HydrationCache's clearAll method which uses SCAN instead of KEYS 728 + try { 729 + await this.cache.clearAll(); 730 + } catch (error) { 731 + console.error('[OPTIMIZED_HYDRATION] Cache clear error:', error); 736 732 } 737 733 } 738 734