Files
Spanglish/backend/src/lib/stores/cache.ts
T
MichilisandCursor 613bd7be1d Refactor monolithic modules and harden booking, email, and auth infrastructure.
Split oversized frontend API client, email service, and admin/booking pages into focused modules while preserving import surfaces, and add Redis-backed queues, stale booking cleanup, stronger auth, and scale deployment configs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 07:12:59 +00:00

106 lines
2.9 KiB
TypeScript

// Cache abstraction with two implementations:
// - memory: per-process Map with TTL expiry (single instance)
// - redis: shared GET / SETEX / DEL with JSON values (all instances)
//
// Values are JSON-serialized. Selection happens once based on REDIS_URL. On any
// Redis error the cache behaves as a miss so callers fall back to their source.
import { getRedis, isRedisEnabled } from '../redis.js';
export interface Cache {
readonly backend: 'memory' | 'redis';
get<T>(key: string): Promise<T | null>;
set<T>(key: string, value: T, ttlSeconds: number): Promise<void>;
del(key: string): Promise<void>;
}
// ==================== Memory implementation ====================
interface Entry {
value: unknown;
expiresAt: number;
}
class MemoryCache implements Cache {
readonly backend = 'memory' as const;
private store = new Map<string, Entry>();
constructor() {
const cleanup = setInterval(() => {
const now = Date.now();
for (const [key, entry] of this.store) {
if (now > entry.expiresAt) this.store.delete(key);
}
}, 60_000);
(cleanup as any).unref?.();
}
async get<T>(key: string): Promise<T | null> {
const entry = this.store.get(key);
if (!entry) return null;
if (Date.now() > entry.expiresAt) {
this.store.delete(key);
return null;
}
return entry.value as T;
}
async set<T>(key: string, value: T, ttlSeconds: number): Promise<void> {
this.store.set(key, { value, expiresAt: Date.now() + ttlSeconds * 1000 });
}
async del(key: string): Promise<void> {
this.store.delete(key);
}
}
// ==================== Redis implementation ====================
class RedisCache implements Cache {
readonly backend = 'redis' as const;
async get<T>(key: string): Promise<T | null> {
const redis = getRedis();
if (!redis) return null;
try {
const raw = await redis.get(`cache:${key}`);
if (raw === null) return null;
return JSON.parse(raw) as T;
} catch (err: any) {
console.error('[cache] redis get error:', err?.message || err);
return null;
}
}
async set<T>(key: string, value: T, ttlSeconds: number): Promise<void> {
const redis = getRedis();
if (!redis) return;
try {
await redis.set(`cache:${key}`, JSON.stringify(value), 'EX', ttlSeconds);
} catch (err: any) {
console.error('[cache] redis set error:', err?.message || err);
}
}
async del(key: string): Promise<void> {
const redis = getRedis();
if (!redis) return;
try {
await redis.del(`cache:${key}`);
} catch (err: any) {
console.error('[cache] redis del error:', err?.message || err);
}
}
}
// ==================== Selection ====================
let instance: Cache | null = null;
export function getCache(): Cache {
if (!instance) {
instance = isRedisEnabled() ? new RedisCache() : new MemoryCache();
}
return instance;
}