254 lines
5.5 KiB
TypeScript
254 lines
5.5 KiB
TypeScript
/**
|
|
* Simple in-memory cache with TTL support
|
|
*
|
|
* Usage:
|
|
* ```typescript
|
|
* const cache = new Cache<User[]>(5 * 60 * 1000); // 5 minutes TTL
|
|
*
|
|
* const users = await cache.getOrSet('all-users', async () => {
|
|
* return await db.getAllUsers();
|
|
* });
|
|
* ```
|
|
*/
|
|
|
|
import log from "../logger";
|
|
|
|
export interface CacheEntry<T> {
|
|
value: T;
|
|
expiresAt: number;
|
|
createdAt: number;
|
|
}
|
|
|
|
export interface CacheStats {
|
|
hits: number;
|
|
misses: number;
|
|
size: number;
|
|
hitRate: number;
|
|
}
|
|
|
|
/**
|
|
* Simple in-memory cache with TTL (Time To Live)
|
|
*/
|
|
export class Cache<T = any> {
|
|
private store: Map<string, CacheEntry<T>> = new Map();
|
|
private hits = 0;
|
|
private misses = 0;
|
|
private defaultTtl: number;
|
|
|
|
/**
|
|
* Create a new cache instance
|
|
*
|
|
* @param defaultTtl - Default time-to-live in milliseconds (default: 5 minutes)
|
|
*/
|
|
constructor(defaultTtl: number = 5 * 60 * 1000) {
|
|
this.defaultTtl = defaultTtl;
|
|
}
|
|
|
|
/**
|
|
* Get a value from cache
|
|
*
|
|
* @param key - Cache key
|
|
* @returns Cached value or null if expired/not found
|
|
*/
|
|
get(key: string): T | null {
|
|
const entry = this.store.get(key);
|
|
|
|
if (!entry) {
|
|
this.misses++;
|
|
return null;
|
|
}
|
|
|
|
// Check if expired
|
|
if (Date.now() > entry.expiresAt) {
|
|
this.store.delete(key);
|
|
this.misses++;
|
|
log.debug("Cache expired", { key });
|
|
return null;
|
|
}
|
|
|
|
this.hits++;
|
|
log.debug("Cache hit", { key });
|
|
return entry.value;
|
|
}
|
|
|
|
/**
|
|
* Set a value in cache
|
|
*
|
|
* @param key - Cache key
|
|
* @param value - Value to cache
|
|
* @param ttl - Time-to-live in milliseconds (optional, uses default if not provided)
|
|
*/
|
|
set(key: string, value: T, ttl?: number): void {
|
|
const expiresAt = Date.now() + (ttl ?? this.defaultTtl);
|
|
this.store.set(key, {
|
|
value,
|
|
expiresAt,
|
|
createdAt: Date.now(),
|
|
});
|
|
log.debug("Cache set", { key, ttl: ttl ?? this.defaultTtl });
|
|
}
|
|
|
|
/**
|
|
* Get a value from cache, or compute and cache it if not found
|
|
*
|
|
* @param key - Cache key
|
|
* @param fn - Function to compute value if not in cache
|
|
* @param ttl - Time-to-live in milliseconds (optional)
|
|
* @returns Cached or computed value
|
|
*/
|
|
async getOrSet(key: string, fn: () => Promise<T>, ttl?: number): Promise<T> {
|
|
const cached = this.get(key);
|
|
if (cached !== null) {
|
|
return cached;
|
|
}
|
|
|
|
log.debug("Cache miss, computing value", { key });
|
|
const value = await fn();
|
|
this.set(key, value, ttl);
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Delete a value from cache
|
|
*
|
|
* @param key - Cache key
|
|
* @returns True if deleted, false if not found
|
|
*/
|
|
delete(key: string): boolean {
|
|
const deleted = this.store.delete(key);
|
|
log.debug("Cache delete", { key, deleted });
|
|
return deleted;
|
|
}
|
|
|
|
/**
|
|
* Clear all entries from cache
|
|
*/
|
|
clear(): void {
|
|
const size = this.store.size;
|
|
this.store.clear();
|
|
this.hits = 0;
|
|
this.misses = 0;
|
|
log.info("Cache cleared", { entriesCleared: size });
|
|
}
|
|
|
|
/**
|
|
* Get cache statistics
|
|
*/
|
|
getStats(): CacheStats {
|
|
const total = this.hits + this.misses;
|
|
return {
|
|
hits: this.hits,
|
|
misses: this.misses,
|
|
size: this.store.size,
|
|
hitRate: total > 0 ? this.hits / total : 0,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Remove expired entries from cache
|
|
*/
|
|
cleanup(): number {
|
|
const now = Date.now();
|
|
let removed = 0;
|
|
|
|
for (const [key, entry] of Array.from(this.store.entries())) {
|
|
if (now > entry.expiresAt) {
|
|
this.store.delete(key);
|
|
removed++;
|
|
}
|
|
}
|
|
|
|
if (removed > 0) {
|
|
log.info("Cache cleanup completed", { entriesRemoved: removed });
|
|
}
|
|
|
|
return removed;
|
|
}
|
|
|
|
/**
|
|
* Check if a key exists in cache (and is not expired)
|
|
*/
|
|
has(key: string): boolean {
|
|
const entry = this.store.get(key);
|
|
if (!entry) return false;
|
|
|
|
if (Date.now() > entry.expiresAt) {
|
|
this.store.delete(key);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get all keys in cache
|
|
*/
|
|
keys(): string[] {
|
|
return Array.from(this.store.keys());
|
|
}
|
|
|
|
/**
|
|
* Get cache size (number of entries)
|
|
*/
|
|
size(): number {
|
|
return this.store.size;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Global cache instances for common use cases
|
|
*/
|
|
|
|
// User data cache (5 minutes)
|
|
export const userCache = new Cache<any>(5 * 60 * 1000);
|
|
|
|
// Fitness goals cache (2 minutes)
|
|
export const fitnessGoalsCache = new Cache<any>(2 * 60 * 1000);
|
|
|
|
// Recommendations cache (3 minutes)
|
|
export const recommendationsCache = new Cache<any>(3 * 60 * 1000);
|
|
|
|
// Dashboard stats cache (1 minute - frequently changing data)
|
|
export const dashboardStatsCache = new Cache<any>(1 * 60 * 1000);
|
|
|
|
/**
|
|
* Setup automatic cache cleanup (run every 5 minutes)
|
|
*/
|
|
export function setupCacheCleanup(): NodeJS.Timeout {
|
|
const interval = setInterval(
|
|
() => {
|
|
log.debug("Running scheduled cache cleanup");
|
|
userCache.cleanup();
|
|
fitnessGoalsCache.cleanup();
|
|
recommendationsCache.cleanup();
|
|
dashboardStatsCache.cleanup();
|
|
},
|
|
5 * 60 * 1000,
|
|
); // Every 5 minutes
|
|
|
|
return interval;
|
|
}
|
|
|
|
/**
|
|
* Clear all caches
|
|
*/
|
|
export function clearAllCaches(): void {
|
|
userCache.clear();
|
|
fitnessGoalsCache.clear();
|
|
recommendationsCache.clear();
|
|
dashboardStatsCache.clear();
|
|
log.info("All caches cleared");
|
|
}
|
|
|
|
/**
|
|
* Get stats for all caches
|
|
*/
|
|
export function getAllCacheStats(): Record<string, CacheStats> {
|
|
return {
|
|
users: userCache.getStats(),
|
|
fitnessGoals: fitnessGoalsCache.getStats(),
|
|
recommendations: recommendationsCache.getStats(),
|
|
dashboardStats: dashboardStatsCache.getStats(),
|
|
};
|
|
}
|