diff --git a/lib/distributed-cache.ts b/lib/distributed-cache.ts new file mode 100644 index 000000000..030fa7401 --- /dev/null +++ b/lib/distributed-cache.ts @@ -0,0 +1,100 @@ +/** + * lib/distributed-cache.ts + * + * Distributed cache layer using Redis for TTL cache shared across instances. + * Prevents data inconsistency in distributed deployments. + */ + +export interface CacheEntry { + data: T; + expiresAt: number; + createdAt: number; +} + +export interface RedisClient { + get(key: string): Promise; + setex(key: string, seconds: number, value: string): Promise; + del(key: string): Promise; + flushdb(): Promise; +} + +export class DistributedCache { + private redisClient: RedisClient | null; + private ttlMs: number; + + constructor(redisClient: RedisClient | null, ttlMs: number = 3600000) { + this.redisClient = redisClient; + this.ttlMs = ttlMs; + } + + async get(key: string): Promise { + if (!this.redisClient) { + return null; + } + + try { + const value = await this.redisClient.get(key); + if (!value) { + return null; + } + + const entry: CacheEntry = JSON.parse(value); + if (entry.expiresAt < Date.now()) { + await this.delete(key); + return null; + } + + return entry.data; + } catch (error) { + console.error(`Cache get error for ${key}:`, error); + return null; + } + } + + async set(key: string, data: T, ttlMs?: number): Promise { + if (!this.redisClient) { + return; + } + + try { + const expiryTime = ttlMs || this.ttlMs; + const entry: CacheEntry = { + data, + expiresAt: Date.now() + expiryTime, + createdAt: Date.now(), + }; + + await this.redisClient.setex(key, Math.ceil(expiryTime / 1000), JSON.stringify(entry)); + } catch (error) { + console.error(`Cache set error for ${key}:`, error); + } + } + + async delete(key: string): Promise { + if (!this.redisClient) { + return; + } + + try { + await this.redisClient.del(key); + } catch (error) { + console.error(`Cache delete error for ${key}:`, error); + } + } + + async clear(): Promise { + if (!this.redisClient) { + return; + } + + try { + await this.redisClient.flushdb(); + } catch (error) { + console.error('Cache clear error:', error); + } + } +} + +export function createDistributedCache(redisClient: RedisClient | null): DistributedCache { + return new DistributedCache(redisClient); +}