enhancment complete with phase 10
This commit is contained in:
1 parent
2c4927478a
commit
6adb541967
44 files changed
+7820
-845
No files matched your search
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* 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(),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user