Monorepo for Aesthetic.Computer
aesthetic.computer
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};