@oxyhq/services
Version:
Reusable OxyHQ module to handle authentication, user management, karma system, device-based session management and more 🚀
265 lines (234 loc) • 5.68 kB
text/typescript
/**
* Centralized cache utility with TTL support
*
* This is a production-ready cache implementation used across the codebase
* for consistent caching behavior and performance optimization.
*/
/**
* Cache entry with expiration tracking
*/
interface CacheEntry<T> {
data: T;
timestamp: number;
expiresAt: number;
}
/**
* Cache statistics
*/
export interface CacheStats {
size: number;
hits: number;
misses: number;
hitRate: number;
}
/**
* TTL-based cache implementation
*
* Features:
* - Automatic expiration based on TTL
* - Manual cleanup of expired entries
* - Statistics tracking (hits, misses, hit rate)
* - Type-safe generic interface
*
* @example
* ```typescript
* const cache = new TTLCache<string>(5 * 60 * 1000); // 5 minutes
*
* // Set with default TTL
* cache.set('key', 'value');
*
* // Set with custom TTL
* cache.set('key', 'value', 10 * 60 * 1000); // 10 minutes
*
* // Get value
* const value = cache.get('key');
*
* // Get statistics
* const stats = cache.getStats();
* ```
*/
export class TTLCache<T> {
private cache = new Map<string, CacheEntry<T>>();
private defaultTTL: number;
private hits = 0;
private misses = 0;
/**
* Create a new TTL cache
* @param defaultTTL Default TTL 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 not found or expired
*/
get(key: string): T | null {
const entry = this.cache.get(key);
if (!entry) {
this.misses++;
return null;
}
const now = Date.now();
if (now > entry.expiresAt) {
this.cache.delete(key);
this.misses++;
return null;
}
this.hits++;
return entry.data;
}
/**
* Set a value in cache
* @param key Cache key
* @param data Data to cache
* @param ttl Optional TTL override (uses default if not provided)
*/
set(key: string, data: T, ttl?: number): void {
const now = Date.now();
const expiresAt = now + (ttl || this.defaultTTL);
this.cache.set(key, { data, timestamp: now, expiresAt });
}
/**
* Delete a specific cache entry
* @param key Cache key
* @returns true if entry was deleted, false if not found
*/
delete(key: string): boolean {
return this.cache.delete(key);
}
/**
* Clear all cache entries
*/
clear(): void {
this.cache.clear();
this.hits = 0;
this.misses = 0;
}
/**
* Check if a key exists and is not expired
* @param key Cache key
* @returns true if key exists and is valid
*/
has(key: string): boolean {
const entry = this.cache.get(key);
if (!entry) return false;
if (Date.now() > entry.expiresAt) {
this.cache.delete(key);
return false;
}
return true;
}
/**
* Get all valid cache keys
* @returns Array of valid cache keys
*/
keys(): string[] {
const now = Date.now();
const validKeys: string[] = [];
for (const [key, entry] of this.cache.entries()) {
if (now <= entry.expiresAt) {
validKeys.push(key);
} else {
this.cache.delete(key);
}
}
return validKeys;
}
/**
* Clean up expired entries
* @returns Number of entries removed
*/
cleanup(): number {
const now = Date.now();
let removed = 0;
for (const [key, entry] of this.cache.entries()) {
if (now > entry.expiresAt) {
this.cache.delete(key);
removed++;
}
}
return removed;
}
/**
* Get cache size (number of entries)
*/
size(): number {
return this.cache.size;
}
/**
* Get cache statistics
*/
getStats(): CacheStats {
const total = this.hits + this.misses;
return {
size: this.cache.size,
hits: this.hits,
misses: this.misses,
hitRate: total > 0 ? this.hits / total : 0,
};
}
/**
* Reset statistics (keeps cache entries)
*/
resetStats(): void {
this.hits = 0;
this.misses = 0;
}
}
/**
* Create a TTL cache instance (convenience function)
*
* @example
* ```typescript
* const cache = createCache<string>(5 * 60 * 1000);
* ```
*/
export function createCache<T>(ttl: number = 5 * 60 * 1000): TTLCache<T> {
return new TTLCache<T>(ttl);
}
/**
* Global cache cleanup interval (runs every minute)
* This helps prevent memory leaks from expired cache entries
*/
let cleanupInterval: NodeJS.Timeout | null = null;
const activeCaches = new Set<TTLCache<any>>();
/**
* Register a cache for automatic cleanup
* @param cache Cache instance to register
*/
export function registerCacheForCleanup(cache: TTLCache<any>): void {
activeCaches.add(cache);
// Start cleanup interval if not already running
if (!cleanupInterval) {
cleanupInterval = setInterval(() => {
for (const cache of activeCaches) {
cache.cleanup();
}
}, 60000); // Every minute
}
}
/**
* Unregister a cache from automatic cleanup
* @param cache Cache instance to unregister
*/
export function unregisterCacheFromCleanup(cache: TTLCache<any>): void {
activeCaches.delete(cache);
// Stop cleanup interval if no caches are registered
if (activeCaches.size === 0 && cleanupInterval) {
clearInterval(cleanupInterval);
cleanupInterval = null;
}
}
/**
* Stop all cleanup intervals (useful for testing)
* This will clear the interval and unregister all caches
*/
export function stopAllCleanupIntervals(): void {
if (cleanupInterval) {
clearInterval(cleanupInterval);
cleanupInterval = null;
}
activeCaches.clear();
}