import { CacheProvider } from '../../interfaces/cache-provider'; import { TieredCacheOptions } from '../../interfaces/cache-options'; import { applyTimeout } from './utils'; /** * Handles read operations for tiered cache */ export class TieredCacheReadStrategy { private providers: Array<{ provider: CacheProvider; options: { writeOnHitFromLowerTier: boolean; ttl?: number; }; }>; private options: TieredCacheOptions; private logger: any; /** * Creates a new TieredCacheReadStrategy * @param providers Array of cache providers * @param options Cache options * @param logger Logger instance */ constructor( providers: Array<{ provider: CacheProvider; options: { writeOnHitFromLowerTier: boolean; ttl?: number; }; }>, options: TieredCacheOptions, logger: any ) { this.providers = providers; this.options = options; this.logger = 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 { let value: T | undefined = undefined; let foundInTier = -1; // Try to get from each tier until found for (let i = 0; i < this.providers.length; i++) { try { const provider = this.providers[i]?.provider; if (!provider) continue; const result = await applyTimeout( provider.get(key), 'get', i, this.options.tierTimeout ); if (result !== undefined) { value = result; foundInTier = i; this.logger.debug(`Cache hit for key: ${key} in tier ${i}`); break; } } catch (error) { this.logger.warn(`Error getting from tier ${i} for key: ${key}`, error); // Continue to next tier on error } } // If not found in any tier if (foundInTier === -1) { this.logger.debug(`Cache miss for key: ${key} in all tiers`); return undefined; } // If found in a lower tier, populate higher tiers if enabled if (foundInTier > 0 && value !== undefined) { this.populateHigherTiers(key, value, foundInTier); } return value; } /** * Populate higher tiers with a value found in a lower tier * @param key Cache key * @param value Value to cache * @param foundInTier Tier index where the value was found */ private async populateHigherTiers(key: string, value: T, foundInTier: number): Promise { // Populate higher tiers in parallel const promises: Promise[] = []; for (let i = 0; i < foundInTier; i++) { const providerConfig = this.providers[i]; // Only write to this tier if configured to do so if (providerConfig && providerConfig.options && providerConfig.options.writeOnHitFromLowerTier) { const ttl = providerConfig.options.ttl || this.options.ttl; promises.push( applyTimeout( providerConfig.provider.set(key, value, ttl), 'set', i, this.options.tierTimeout ).catch(error => { this.logger.warn(`Error populating tier ${i} for key: ${key}`, error); return false; }) ); } } // Wait for all writes to complete if (promises.length > 0) { await Promise.allSettled(promises); this.logger.debug(`Populated ${promises.length} higher tiers for key: ${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 { // Check each tier until found for (let i = 0; i < this.providers.length; i++) { try { const provider = this.providers[i]; if (!provider || !provider.provider) continue; const exists = await applyTimeout( provider.provider.has(key), 'has', i, this.options.tierTimeout ); if (exists) { this.logger.debug(`Cache has key: ${key} in tier ${i}`); return true; } } catch (error) { this.logger.warn(`Error checking tier ${i} for key: ${key}`, error); // Continue to next tier on error } } this.logger.debug(`Cache does not have key: ${key} in any tier`); return false; } /** * 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> { if (keys.length === 0) { return new Map(); } const result = new Map(); const remainingKeys = new Set(keys); // Try each tier until all keys are found or we've checked all tiers for (let i = 0; i < this.providers.length && remainingKeys.size > 0; i++) { try { const tierKeys = Array.from(remainingKeys); const provider = this.providers[i]; if (!provider || !provider.provider) continue; const tierResults = await applyTimeout( provider.provider.getMany(tierKeys), 'getMany', i, this.options.tierTimeout ); // Add found keys to the result and remove from remaining for (const [key, value] of tierResults.entries()) { result.set(key, value); remainingKeys.delete(key); } this.logger.debug(`Cache getMany found ${tierResults.size} keys in tier ${i}`); // Populate higher tiers with values found in this tier if (i > 0 && tierResults.size > 0) { this.populateHigherTiersMany(tierResults, i); } } catch (error) { this.logger.warn(`Error in getMany from tier ${i}`, error); // Continue to next tier on error } } this.logger.debug(`Cache getMany for ${keys.length} keys, found ${result.size}`); return result; } /** * Populate higher tiers with multiple values found in a lower tier * @param entries Map of key-value pairs to cache * @param foundInTier Tier index where the values were found */ private async populateHigherTiersMany( entries: Map, foundInTier: number ): Promise { // Only process if we have higher tiers if (foundInTier <= 0 || entries.size === 0) { return; } // Populate higher tiers in parallel const promises: Promise[] = []; for (let i = 0; i < foundInTier; i++) { const providerConfig = this.providers[i]; // Only write to this tier if configured to do so if (providerConfig && providerConfig.options && providerConfig.options.writeOnHitFromLowerTier) { const ttl = providerConfig.options.ttl || this.options.ttl; promises.push( applyTimeout( providerConfig.provider.setMany(entries, ttl), 'setMany', i, this.options.tierTimeout ).catch(error => { this.logger.warn(`Error populating tier ${i} with multiple keys`, error); return false; }) ); } } // Don't await this operation, let it run in the background Promise.allSettled(promises).then(results => { const successful = results.filter(r => r.status === 'fulfilled' && r.value === true).length; this.logger.debug(`Populated ${successful}/${promises.length} higher tiers with ${entries.size} 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 { // Check each tier until found for (let i = 0; i < this.providers.length; i++) { try { const provider = this.providers[i]; if (!provider || !provider.provider) continue; const ttl = await applyTimeout( provider.provider.getTtl(key), 'getTtl', i, this.options.tierTimeout ); if (ttl !== undefined) { this.logger.debug(`Cache TTL for key: ${key} in tier ${i} is ${ttl}ms`); return ttl; } } catch (error) { this.logger.warn(`Error getting TTL from tier ${i} for key: ${key}`, error); // Continue to next tier on error } } this.logger.debug(`Cache TTL not found for key: ${key} in any tier`); return undefined; } }