import { CacheProvider, CacheStats } from './interfaces/cache-provider'; import { CacheOptions, InvalidationOptions } from './interfaces/cache-options'; import { LockManager, LockOptions } from './interfaces/lock'; import { MemoryCache } from './providers/memory-cache'; import { RedisCache } from './providers/redis-cache'; import { TieredCache } from './providers/tiered-cache'; import { createInvalidator } from './invalidation'; import { createLockManager } from './locks'; import { generateKey } from './utils/key-generator'; import { getLogger } from './utils/logger'; /** * CacheManager provides a unified interface to various caching strategies */ export class CacheManager { private provider: CacheProvider; private options: CacheOptions; private invalidator: any; private lockManager?: LockManager; private logger: any; /** * Creates a new CacheManager instance * @param options Cache options */ constructor(options: CacheOptions = {}) { this.options = { ttl: 3600000, // 1 hour keyPrefix: 'cache:', enableLogging: false, logLevel: 'info', ...options }; // Initialize logger this.logger = getLogger('cache:manager', this.options); // Initialize cache provider this.provider = this.createProvider(); // Initialize invalidator if specified if (options.invalidation && options.invalidation.method) { this.invalidator = createInvalidator( options.invalidation.method, this.provider, options.invalidation ); } } /** * Create the appropriate cache provider based on options */ private createProvider(): CacheProvider { // If tiered caching is configured if (this.options.tiered && this.options.tiered.providers && this.options.tiered.providers.length > 0) { this.logger.info('Creating tiered cache provider'); return new TieredCache(this.options.tiered); } // If Redis is configured if (this.options.redis) { this.logger.info('Creating Redis cache provider'); return new RedisCache(this.options.redis); } // Default to in-memory cache this.logger.info('Creating in-memory cache provider'); return new MemoryCache(this.options.memory); } /** * Get a value from the cache * @param key Cache key * @returns Promise resolving to the cached value or undefined if not found */ async get(key: string): Promise { const cacheKey = this.formatKey(key); return this.provider.get(cacheKey); } /** * Set a value in the cache * @param key Cache key * @param value Value to cache * @param ttl Time-to-live in milliseconds (optional) * @param invalidationOptions Optional invalidation options * @returns Promise resolving to true if successful */ async set( key: string, value: T, ttl?: number, invalidationOptions?: InvalidationOptions ): Promise { const cacheKey = this.formatKey(key); const effectiveTtl = ttl || this.options.ttl; // Use invalidator if specified if (invalidationOptions && this.invalidator) { if (invalidationOptions.method === 'tag' && invalidationOptions.tags) { // Set with tags return await this.invalidator.set(cacheKey, value, invalidationOptions.tags, effectiveTtl); } else if (invalidationOptions.method === 'version' && invalidationOptions.version) { // Set with version return await this.invalidator.set(cacheKey, value, invalidationOptions.version, effectiveTtl); } } // Default set return await this.provider.set(cacheKey, value, effectiveTtl); } /** * Delete a value from the cache * @param key Cache key * @returns Promise resolving to true if successful */ async delete(key: string): Promise { const cacheKey = this.formatKey(key); return await this.provider.delete(cacheKey); } /** * Check if a key exists in the cache * @param key Cache key * @returns Promise resolving to true if the key exists */ async has(key: string): Promise { const cacheKey = this.formatKey(key); return await this.provider.has(cacheKey); } /** * Clear all values from the cache * @returns Promise resolving to true if successful */ async clear(): Promise { return await this.provider.clear(); } /** * Get multiple values from the cache * @param keys Array of cache keys * @returns Promise resolving to a map of key-value pairs for found items */ async getMany(keys: string[]): Promise> { const cacheKeys = keys.map(key => this.formatKey(key)); const resultMap = await this.provider.getMany(cacheKeys); // Convert back to original keys const originalKeyMap = new Map(); for (const [cacheKey, value] of resultMap.entries()) { const originalKey = this.getOriginalKey(cacheKey); originalKeyMap.set(originalKey, value); } return originalKeyMap; } /** * Set multiple values in the cache * @param entries Map of key-value pairs to cache * @param ttl Time-to-live in milliseconds (optional) * @returns Promise resolving to true if successful */ async setMany(entries: Map, ttl?: number): Promise { const cacheEntries = new Map(); for (const [key, value] of entries.entries()) { cacheEntries.set(this.formatKey(key), value); } return await this.provider.setMany(cacheEntries, ttl || this.options.ttl); } /** * Delete multiple values from the cache * @param keys Array of cache keys to delete * @returns Promise resolving to true if successful */ async deleteMany(keys: string[]): Promise { const cacheKeys = keys.map(key => this.formatKey(key)); return await this.provider.deleteMany(cacheKeys); } /** * Get or set a value in the cache (retrieve if exists, otherwise generate and store) * @param key Cache key * @param factory Function to generate value if not found in cache * @param ttl Time-to-live in milliseconds (optional) * @param invalidationOptions Optional invalidation options * @returns Promise resolving to the value */ async getOrSet( key: string, factory: () => Promise, ttl?: number, invalidationOptions?: InvalidationOptions ): Promise { // Try to get from cache first const cached = await this.get(key); if (cached !== undefined) { return cached; } // Generate the value const value = await factory(); // Store in cache await this.set(key, value, ttl, invalidationOptions); return value; } /** * Wrap a function with caching * @param fn Function to wrap * @param keyGenerator Function to generate cache key from args * @param ttl Time-to-live in milliseconds (optional) * @param invalidationOptions Optional invalidation options * @returns Wrapped function with caching */ wrap( fn: (...args: A) => Promise, keyGenerator: (...args: A) => string, ttl?: number, invalidationOptions?: InvalidationOptions ): (...args: A) => Promise { return async (...args: A): Promise => { const key = keyGenerator(...args); return this.getOrSet( key, () => fn(...args), ttl, invalidationOptions ); }; } /** * Wrap a function with caching and lock for exclusive access * @param fn Function to wrap * @param keyGenerator Function to generate cache key from args * @param lockOptions Options for locking * @param ttl Time-to-live in milliseconds (optional) * @param invalidationOptions Optional invalidation options * @returns Wrapped function with caching and locking */ wrapWithLock( fn: (...args: A) => Promise, keyGenerator: (...args: A) => string, lockOptions: LockOptions & { ttl?: number } = {}, ttl?: number, invalidationOptions?: InvalidationOptions ): (...args: A) => Promise { // Ensure lock manager is initialized this.ensureLockManager(); return async (...args: A): Promise => { const key = keyGenerator(...args); const lockKey = `lock:${key}`; const lockTtl = lockOptions.ttl || 10000; // Default to 10 seconds // Remove ttl from lock options const { ttl: _, ...actualLockOptions } = lockOptions; return this.getLockManager().withLock( lockKey, async () => { return this.getOrSet( key, () => fn(...args), ttl, invalidationOptions ); }, lockTtl, actualLockOptions ); }; } /** * Invalidate cache entries * @param pattern Pattern to match (for pattern invalidation) * @param tags Tags to invalidate (for tag invalidation) * @param namespace Namespace to invalidate (for version invalidation) * @returns Promise resolving to the number of invalidated entries */ async invalidate( pattern?: string, tags?: string[], namespace?: string ): Promise { if (!this.invalidator) { throw new Error('No invalidator configured'); } if (this.options.invalidation?.method === 'pattern' && pattern) { return await this.invalidator.invalidateByPattern(pattern); } else if (this.options.invalidation?.method === 'tag' && tags) { if (tags.length === 1) { return await this.invalidator.invalidateByTag(tags[0]); } else { return await this.invalidator.invalidateByTags(tags); } } else if (this.options.invalidation?.method === 'version' && namespace) { await this.invalidator.invalidateNamespace(namespace); return 1; // Version invalidation doesn't return count } throw new Error('Invalid invalidation parameters'); } /** * Get cache stats * @returns Promise resolving to cache statistics */ async getStats(): Promise { return await this.provider.getStats(); } /** * Connect to the cache backend * @returns Promise resolving when connected */ async connect(): Promise { await this.provider.connect(); // Initialize invalidator if needed if (this.invalidator && typeof this.invalidator.init === 'function') { await this.invalidator.init(); } } /** * Disconnect from the cache backend * @returns Promise resolving when disconnected */ async disconnect(): Promise { await this.provider.disconnect(); } /** * Ensure lock manager is initialized */ private ensureLockManager(): void { if (!this.lockManager) { if (this.options.redis) { // Use Redis-based lock manager if Redis is configured this.lockManager = createLockManager('redis', { client: (this.provider as any).client, enableLogging: this.options.enableLogging, logLevel: this.options.logLevel, }); } else { // Fall back to memory-based lock manager this.lockManager = createLockManager('memory', { enableLogging: this.options.enableLogging, logLevel: this.options.logLevel, }); } } } /** * Get the lock manager instance * @returns Lock manager instance */ getLockManager(): LockManager { this.ensureLockManager(); return this.lockManager!; } /** * Format a key according to configured prefix * @param key Original key * @returns Formatted key */ private formatKey(key: string): string { return `${this.options.keyPrefix}${key}`; } /** * Get the original key from a formatted key * @param formattedKey Formatted key * @returns Original key */ private getOriginalKey(formattedKey: string): string { return formattedKey.substring(this.options.keyPrefix?.length || 0); } /** * Generate a cache key from an object * @param obj Object to generate key from * @param options Key generation options * @returns Generated cache key */ static generateKey(obj: any, options?: any): string { return generateKey(obj, options); } }