Monorepo for Aesthetic.Computer aesthetic.computer
at main 131 lines 3.6 kB view raw
1// Cache, 2026.01.04 2// Redis-backed caching for expensive database operations. 3// Uses TTL-based expiration to reduce load on MongoDB. 4 5import { createClient } from "redis"; 6 7const redisConnectionString = process.env.REDIS_CONNECTION_STRING; 8const dev = process.env.NETLIFY_DEV; 9 10let client; 11 12async function connect() { 13 if (client && client.isOpen) { 14 return client; 15 } 16 client = !dev ? createClient({ url: redisConnectionString }) : createClient(); 17 client.on("error", (err) => console.log("🔴 Cache Redis error:", err)); 18 await client.connect(); 19 return client; 20} 21 22async function disconnect() { 23 if (!client?.isOpen) return; 24 await client.quit(); 25} 26 27/** 28 * Get a cached value, or compute and cache it if missing/expired. 29 * 30 * @param {string} key - Cache key (e.g., "metrics:handles") 31 * @param {function} computeFn - Async function to compute value if cache miss 32 * @param {number} ttlSeconds - Time to live in seconds (default: 30 minutes) 33 * @returns {Promise<any>} - The cached or computed value 34 */ 35async function getOrCompute(key, computeFn, ttlSeconds = 1800) { 36 // Try reading from Redis cache first 37 let cacheAvailable = false; 38 try { 39 await connect(); 40 const cached = await client.get(key); 41 if (cached) { 42 console.log(`📦 Cache HIT: ${key}`); 43 return JSON.parse(cached); 44 } 45 console.log(`📭 Cache MISS: ${key}`); 46 cacheAvailable = true; 47 } catch (err) { 48 console.error(`⚠️ Cache read error for ${key}:`, err.message); 49 } 50 51 // Compute the value (let errors propagate to caller) 52 const value = await computeFn(); 53 54 // Try to store in cache (non-blocking, don't fail if Redis is down) 55 if (cacheAvailable) { 56 try { 57 await client.setEx(key, ttlSeconds, JSON.stringify(value)); 58 } catch (err) { 59 console.error(`⚠️ Cache write error for ${key}:`, err.message); 60 } 61 } 62 63 return value; 64} 65 66/** 67 * Invalidate a cache key or pattern. 68 * 69 * @param {string} key - Exact key or pattern with * wildcard 70 */ 71async function invalidate(key) { 72 try { 73 await connect(); 74 75 if (key.includes('*')) { 76 // Pattern-based deletion 77 const keys = await client.keys(key); 78 if (keys.length > 0) { 79 await client.del(keys); 80 console.log(`🗑️ Cache invalidated ${keys.length} keys matching: ${key}`); 81 } 82 } else { 83 await client.del(key); 84 console.log(`🗑️ Cache invalidated: ${key}`); 85 } 86 } catch (err) { 87 console.error(`⚠️ Cache invalidate error:`, err.message); 88 } 89} 90 91/** 92 * Get cache stats for debugging. 93 */ 94async function stats() { 95 try { 96 await connect(); 97 const info = await client.info('memory'); 98 const keys = await client.dbSize(); 99 return { keys, memoryInfo: info }; 100 } catch (err) { 101 return { error: err.message }; 102 } 103} 104 105// Pre-defined cache keys and TTLs 106const CACHE_KEYS = { 107 METRICS: 'give:metrics', // 30 min - platform stats 108 KIDLISP_COUNT: 'give:kidlisp', // 30 min - kidlisp program count 109 TV_RECENT: 'give:tv:recent', // 5 min - recent tapes 110 CHAT_CLOCK: 'give:chat:clock', // 2 min - clock chat messages 111 CHAT_SYSTEM: 'give:chat:system', // 2 min - system chat messages 112 SHOP: 'give:shop', // 10 min - shop items 113}; 114 115const CACHE_TTLS = { 116 METRICS: 30 * 60, // 30 minutes 117 KIDLISP_COUNT: 30 * 60, // 30 minutes 118 TV_RECENT: 5 * 60, // 5 minutes 119 CHAT: 2 * 60, // 2 minutes 120 SHOP: 10 * 60, // 10 minutes 121}; 122 123export { 124 connect, 125 disconnect, 126 getOrCompute, 127 invalidate, 128 stats, 129 CACHE_KEYS, 130 CACHE_TTLS 131};