From 4b065a4062c88fc56e432beb254b4564b45fefb4 Mon Sep 17 00:00:00 2001 From: arsam Date: Fri, 18 Sep 2026 23:19:11 +0330 Subject: [PATCH] Import source: app/backend/src/lib/cache Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality. --- app/backend/src/lib/cache.ts | 54 ++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 app/backend/src/lib/cache.ts diff --git a/app/backend/src/lib/cache.ts b/app/backend/src/lib/cache.ts new file mode 100644 index 0000000..331ae27 --- /dev/null +++ b/app/backend/src/lib/cache.ts @@ -0,0 +1,54 @@ +import { redis } from '../redis.js'; + +export interface CacheOptions { + ttlSeconds: number; + graceSeconds?: number; +} + +export async function cached(key: string, ttlSeconds: number | CacheOptions, fetcher: () => Promise): Promise { + const opts: CacheOptions = typeof ttlSeconds === 'number' + ? { ttlSeconds } + : ttlSeconds; + + const graceSeconds = opts.graceSeconds ?? Math.max(Math.floor(opts.ttlSeconds * 0.25), 30); + const totalTtl = opts.ttlSeconds + graceSeconds; + + const hit = await redis.get(key); + if (hit !== null) { + try { + const parsed = JSON.parse(hit) as T; + const ttlRemaining = await redis.ttl(key); + if (ttlRemaining >= 0 && ttlRemaining <= graceSeconds) { + fetcher() + .then((fresh) => { + if (fresh !== undefined) { + redis.set(key, JSON.stringify(fresh), 'EX', totalTtl).catch(() => {}); + } + }) + .catch(() => {}); + } + return parsed; + } catch { + await redis.del(key); + } + } + + const fresh = await fetcher(); + if (fresh !== undefined) { + await redis.set(key, JSON.stringify(fresh), 'EX', totalTtl); + } + return fresh; +} + +export async function bust(pattern: string): Promise { + let cursor = '0'; + let removed = 0; + do { + const [next, batch] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100); + cursor = next; + if (batch.length > 0) { + removed += await redis.del(...batch); + } + } while (cursor !== '0'); + return removed; +}