import { CacheProvider, CacheStats, TieredCacheOptions } from '../interfaces'; import { TieredCacheReadStrategy } from './tiered/read-strategy'; import { TieredCacheWriteStrategy } from './tiered/write-strategy'; import { TieredCachePropagator } from './tiered/propagator'; import { getLogger } from '../utils'; /** * Tiered cache provider implementation * Manages multiple cache providers in layers (L1, L2, etc.) */ export class TieredCache implements CacheProvider { private providers: Array<{ provider: CacheProvider; options: { writeOnHitFromLowerTier: boolean; ttl?: number; }; }>; private options: TieredCacheOptions; private logger: any; private readStrategy: TieredCacheReadStrategy; private writeStrategy: TieredCacheWriteStrategy; private propagator: TieredCachePropagator; /** * Creates a new TieredCache instance * @param options Tiered cache options */ constructor(options: TieredCacheOptions) { this.options = { ttl: 3600000, // 1 hour default keyPrefix: 'cache:', serialize: true, enableLogging: false, logLevel: 'info', writeStrategy: 'write-through', propagateDeletes: true, propagateUpdates: true, tierTimeout: 5000, // 5 seconds ...options, }; if (!this.options.providers || this.options.providers.length === 0) { throw new Error('At least one cache provider is required for tiered caching'); } // Initialize providers with defaults this.providers = this.options.providers.map(p => ({ provider: p.provider, options: { writeOnHitFromLowerTier: p.options?.writeOnHitFromLowerTier !== false, ttl: p.options?.ttl, }, })); // Initialize logger this.logger = getLogger('cache:tiered', this.options); // Initialize strategies this.readStrategy = new TieredCacheReadStrategy( this.providers, this.options, this.logger ); this.writeStrategy = new TieredCacheWriteStrategy( this.providers, this.options, this.logger ); this.propagator = new TieredCachePropagator( this.providers, this.options, this.logger ); } /** * 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 { return this.readStrategy.get(key); } /** * Set a value in the cache * @param key Cache key * @param value Value to cache * @param ttl Time-to-live in milliseconds (optional) * @returns Promise resolving to true if successful */ async set(key: string, value: T, ttl?: number): Promise { return this.writeStrategy.set(key, value, ttl); } /** * Delete a value from the cache * @param key Cache key * @returns Promise resolving to true if successful */ async delete(key: string): Promise { return this.propagator.delete(key); } /** * 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 { return this.readStrategy.has(key); } /** * Clear all values from the cache * @returns Promise resolving to true if successful */ async clear(): Promise { return this.propagator.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> { return this.readStrategy.getMany(keys); } /** * 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 { return this.writeStrategy.setMany(entries, 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 { return this.propagator.deleteMany(keys); } /** * Get the Time-To-Live (TTL) for a cache key * @param key Cache key * @returns Promise resolving to TTL in milliseconds, or undefined if not found */ async getTtl(key: string): Promise { return this.readStrategy.getTtl(key); } /** * Update the Time-To-Live (TTL) for a cache key * @param key Cache key * @param ttl New TTL in milliseconds * @returns Promise resolving to true if successful */ async updateTtl(key: string, ttl: number): Promise { return this.propagator.updateTtl(key, ttl); } /** * Increment a numeric value in the cache * @param key Cache key * @param increment Amount to increment (default: 1) * @returns Promise resolving to the new value */ async increment(key: string, increment: number = 1): Promise { return this.writeStrategy.increment(key, increment); } /** * Decrement a numeric value in the cache * @param key Cache key * @param decrement Amount to decrement (default: 1) * @returns Promise resolving to the new value */ async decrement(key: string, decrement: number = 1): Promise { return this.increment(key, -decrement); } /** * Connect to all cache providers * @returns Promise resolving when all providers are connected */ async connect(): Promise { const promises = this.providers.map((provider, index) => { return provider.provider.connect().catch(error => { this.logger.error(`Failed to connect to tier ${index}`, error); throw error; }); }); await Promise.all(promises); this.logger.info(`Connected to ${this.providers.length} cache tiers`); } /** * Disconnect from all cache providers * @returns Promise resolving when all providers are disconnected */ async disconnect(): Promise { const promises = this.providers.map((provider, index) => { return provider.provider.disconnect().catch(error => { this.logger.error(`Failed to disconnect from tier ${index}`, error); }); }); await Promise.allSettled(promises); this.logger.info('Disconnected from all cache tiers'); } /** * Get cache stats * @returns Promise resolving to cache statistics */ async getStats(): Promise { // Collect stats from all tiers const promises = this.providers.map((provider, index) => { return provider.provider.getStats().catch(error => { this.logger.error(`Failed to get stats from tier ${index}`, error); return { size: 0, hits: 0, misses: 0, hitRatio: 0, tier: index, }; }); }); const stats = await Promise.all(promises); // Aggregate stats const tierStats = stats.map((stat, index) => ({ ...stat, tier: index })); const combinedStats: CacheStats = { size: tierStats.reduce((sum, stat) => sum + stat.size, 0), hits: tierStats.reduce((sum, stat) => sum + stat.hits, 0), misses: tierStats.reduce((sum, stat) => sum + stat.misses, 0), hitRatio: 0, tiers: tierStats, }; // Calculate overall hit ratio const total = combinedStats.hits + combinedStats.misses; combinedStats.hitRatio = total > 0 ? combinedStats.hits / total : 0; return combinedStats; } }