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>
106 lines
2.9 KiB
TypeScript
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;
|
|
}
|