an app to share curated trails sidetrail.app
1

Configure Feed

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

at bug-repro 5.0 kB View raw
1const Redis = require("ioredis"); 2const { LRUCache } = require("lru-cache"); 3 4const CACHE_PREFIX = "nextcache:"; 5const TAGS_PREFIX = "nextcache:tags:"; 6const LRU_MAX_SIZE = parseInt(process.env.LRU_CACHE_MAX_SIZE || "50", 10) * 1024 * 1024; 7 8const memoryCache = new LRUCache({ 9 maxSize: LRU_MAX_SIZE, 10 sizeCalculation: (entry) => entry.size, 11}); 12 13let redis = null; 14const pendingSets = new Map(); 15 16function getRedis() { 17 if (!redis) { 18 redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379", { 19 maxRetriesPerRequest: 3, 20 lazyConnect: true, 21 }); 22 redis.on("error", (err) => { 23 console.error("[redis] connection error:", err.message); 24 }); 25 } 26 return redis; 27} 28 29async function streamToBuffer(stream) { 30 const reader = stream.getReader(); 31 const chunks = []; 32 try { 33 while (true) { 34 const { done, value } = await reader.read(); 35 if (done) break; 36 chunks.push(value); 37 } 38 } finally { 39 reader.releaseLock(); 40 } 41 return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))); 42} 43 44function bufferToStream(buffer) { 45 return new ReadableStream({ 46 start(controller) { 47 controller.enqueue(buffer); 48 controller.close(); 49 }, 50 }); 51} 52 53async function areTagsExpired(tags, timestamp) { 54 if (!tags?.length) return false; 55 try { 56 const values = await getRedis().mget(tags.map((t) => TAGS_PREFIX + t)); 57 return values.some((val) => val && parseInt(val, 10) > timestamp); 58 } catch { 59 return false; 60 } 61} 62 63module.exports = { 64 async get(cacheKey) { 65 const pending = pendingSets.get(cacheKey); 66 if (pending) await pending; 67 68 const now = Date.now(); 69 70 // LRU 71 const mem = memoryCache.get(cacheKey); 72 if (mem) { 73 const entry = mem.entry; 74 if (now > entry.timestamp + entry.revalidate * 1000) { 75 memoryCache.delete(cacheKey); 76 } else if (await areTagsExpired(entry.tags, entry.timestamp)) { 77 memoryCache.delete(cacheKey); 78 } else { 79 const [ret, keep] = entry.value.tee(); 80 entry.value = keep; 81 return { ...entry, value: ret }; 82 } 83 } 84 85 // Redis 86 try { 87 const stored = await getRedis().get(CACHE_PREFIX + cacheKey); 88 if (!stored) { 89 return undefined; 90 } 91 92 const data = JSON.parse(stored); 93 94 if (now > data.timestamp + data.revalidate * 1000) { 95 await getRedis().del(CACHE_PREFIX + cacheKey); 96 return undefined; 97 } 98 99 if (await areTagsExpired(data.tags, data.timestamp)) { 100 await getRedis().del(CACHE_PREFIX + cacheKey); 101 return undefined; 102 } 103 104 const buffer = Buffer.from(data.value, "base64"); 105 const entry = { 106 value: bufferToStream(buffer), 107 tags: data.tags, 108 stale: data.stale, 109 timestamp: data.timestamp, 110 expire: data.expire, 111 revalidate: data.revalidate, 112 }; 113 114 const [ret, keep] = entry.value.tee(); 115 entry.value = keep; 116 memoryCache.set(cacheKey, { entry, size: buffer.byteLength }); 117 118 return { ...entry, value: ret }; 119 } catch (err) { 120 console.error("[redis] get error:", err.message); 121 return undefined; 122 } 123 }, 124 125 async set(cacheKey, pendingEntry) { 126 let resolve; 127 const promise = new Promise((r) => (resolve = r)); 128 pendingSets.set(cacheKey, promise); 129 130 try { 131 const entry = await pendingEntry; 132 const buffer = await streamToBuffer(entry.value); 133 134 memoryCache.set(cacheKey, { 135 entry: { ...entry, value: bufferToStream(buffer) }, 136 size: buffer.byteLength, 137 }); 138 139 const ttl = entry.expire > 0 ? entry.expire : 86400; 140 await getRedis().set( 141 CACHE_PREFIX + cacheKey, 142 JSON.stringify({ 143 value: buffer.toString("base64"), 144 tags: entry.tags, 145 stale: entry.stale, 146 timestamp: entry.timestamp, 147 expire: entry.expire, 148 revalidate: entry.revalidate, 149 }), 150 "EX", 151 ttl, 152 ); 153 } catch (err) { 154 console.error("[redis] set error:", err.message); 155 memoryCache.delete(cacheKey); 156 } finally { 157 resolve(); 158 pendingSets.delete(cacheKey); 159 } 160 }, 161 162 async refreshTags() {}, 163 164 async getExpiration(tags) { 165 if (!tags?.length) return 0; 166 try { 167 const values = await getRedis().mget(tags.map((t) => TAGS_PREFIX + t)); 168 return Math.max(0, ...values.map((v) => (v ? parseInt(v, 10) : 0))); 169 } catch { 170 return 0; 171 } 172 }, 173 174 async updateTags(tags) { 175 if (!tags?.length) return; 176 try { 177 const now = Date.now(); 178 const pipeline = getRedis().pipeline(); 179 for (const tag of tags) { 180 pipeline.set(TAGS_PREFIX + tag, now.toString(), "EX", 604800); 181 } 182 await pipeline.exec(); 183 184 for (const [key, cached] of memoryCache.entries()) { 185 if (cached.entry.tags?.some((t) => tags.includes(t))) { 186 memoryCache.delete(key); 187 } 188 } 189 } catch (err) { 190 console.error("[redis] updateTags error:", err.message); 191 } 192 }, 193};