// Story 7.3: Redis caching service for featured creators // Implements stale-while-revalidate pattern from Story 7.2 import { redis } from '@/lib/redis'; import type { FeaturedCreator, CachedFeaturedCreators } from '../types/discovery.types'; // Dual-key caching strategy (same pattern as trending) const CACHE_TTL_PRIMARY = 7200; // 2 hours in seconds const CACHE_TTL_STALE = 14400; // 4 hours in seconds const CACHE_KEY = 'featured:creators:v1'; const CACHE_KEY_STALE_SUFFIX = ':stale'; /** * Get cached featured creators with stale-while-revalidate pattern * * Flow: * 1. Check primary cache first (2-hour TTL) * 2. If miss, check stale cache (4-hour TTL) * 3. If stale hit, return stale data (caller should trigger background refresh) * 4. If both miss, return null * * Returns: { data, isStale } where isStale indicates if serving from stale cache */ export async function getCachedFeaturedCreators(): Promise<{ data: FeaturedCreator[]; isStale: boolean } | null> { try { // Try primary cache first const primaryCached = await redis.get(CACHE_KEY); if (primaryCached) { return { data: primaryCached.data, isStale: false }; } // Primary cache miss - try stale cache const staleCached = await redis.get( `${CACHE_KEY}${CACHE_KEY_STALE_SUFFIX}` ); if (staleCached) { // Serving stale data - caller should trigger background revalidation return { data: staleCached.data, isStale: true }; } // Both caches missed return null; } catch (error) { console.error('Error reading from Redis cache:', error); // Gracefully degrade to database on Redis failure return null; } } /** * Set cached featured creators with dual-key strategy * Writes to both primary (2h TTL) and stale (4h TTL) caches */ export async function setCachedFeaturedCreators(data: FeaturedCreator[]): Promise { try { const now = Date.now(); const cachedData: CachedFeaturedCreators = { data, cachedAt: now, expiresAt: now + (CACHE_TTL_PRIMARY * 1000), }; // Write to primary cache (2 hour TTL) await redis.set(CACHE_KEY, cachedData, { ex: CACHE_TTL_PRIMARY, }); // Write to stale cache (4 hour TTL) await redis.set( `${CACHE_KEY}${CACHE_KEY_STALE_SUFFIX}`, cachedData, { ex: CACHE_TTL_STALE, } ); } catch (error) { console.error('Error writing to Redis cache:', error); // Don't throw - caching is optional, app should work without it } } /** * Invalidate featured creators cache (both primary and stale) * Useful for manual cache busting or after data updates */ export async function invalidateFeaturedCreatorsCache(): Promise { try { await redis.del(CACHE_KEY); await redis.del(`${CACHE_KEY}${CACHE_KEY_STALE_SUFFIX}`); } catch (error) { console.error('Error invalidating Redis cache:', error); } } /** * Check if primary cache exists and is fresh */ export async function isCacheFresh(): Promise { try { const cached = await redis.get(CACHE_KEY); if (!cached) { return false; } const now = Date.now(); return now <= cached.expiresAt; } catch (error) { console.error('Error checking cache freshness:', error); return false; } }