import { CacheProvider, TieredCacheOptions } from '../../interfaces'; import { applyTimeout } from './utils'; /** * Handles operations that need to propagate across tiers */ export class TieredCachePropagator { private providers: Array<{ provider: CacheProvider; options: { writeOnHitFromLowerTier: boolean; ttl?: number; }; }>; private options: TieredCacheOptions; private logger: any; /** * Creates a new TieredCachePropagator * @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; } /** * Delete a value from the cache * @param key Cache key * @returns Promise resolving to true if successful */ async delete(key: string): Promise { // Delete from first tier first let success = false; // Check if providers array is not empty if (this.providers.length === 0) { this.logger.warn('No cache providers available for delete operation'); return false; } const firstProvider = this.providers[0]; if (!firstProvider || !firstProvider.provider) { this.logger.error(`First provider unavailable for delete operation on key: ${key}`); return false; } try { success = await applyTimeout( firstProvider.provider.delete(key), 'delete', 0, this.options.tierTimeout ); } catch (error) { this.logger.error(`Failed to delete from first tier for key: ${key}`, error); } // Delete from other tiers if propagation is enabled if (this.options.propagateDeletes && this.providers.length > 1) { const promises = this.providers.slice(1).map((provider, index) => { const tierIndex = index + 1; // Adjust for logging if (!provider || !provider.provider) { this.logger.warn(`Provider at tier ${tierIndex} unavailable for delete operation`); return Promise.resolve(false); } return applyTimeout( provider.provider.delete(key), 'delete', tierIndex, this.options.tierTimeout ).catch(error => { this.logger.warn(`Error deleting from tier ${tierIndex} for key: ${key}`, error); return false; }); }); // Don't wait for lower tier deletes if first tier was successful if (success) { Promise.all(promises).catch(error => { this.logger.error(`Error in propagated deletes for key: ${key}`, error); }); } else { // If first tier failed, wait for others const results = await Promise.all(promises); // If any tier deleted successfully, consider it a success success = success || results.some(result => result === true); } } this.logger.debug(`Cache delete for key: ${key}`, { success, propagated: this.options.propagateDeletes }); return success; } /** * 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 { if (keys.length === 0) { return true; } // Check if providers array is not empty if (this.providers.length === 0) { this.logger.warn('No cache providers available for deleteMany operation'); return false; } const firstProvider = this.providers[0]; if (!firstProvider || !firstProvider.provider) { this.logger.error(`First provider unavailable for deleteMany operation`); return false; } // Delete from first tier first let success = false; try { success = await applyTimeout( firstProvider.provider.deleteMany(keys), 'deleteMany', 0, this.options.tierTimeout ); } catch (error) { this.logger.error(`Failed to delete multiple keys from first tier`, error); } // Delete from other tiers if propagation is enabled if (this.options.propagateDeletes && this.providers.length > 1) { const promises = this.providers.slice(1).map((provider, index) => { const tierIndex = index + 1; // Adjust for logging if (!provider || !provider.provider) { this.logger.warn(`Provider at tier ${tierIndex} unavailable for deleteMany operation`); return Promise.resolve(false); } return applyTimeout( provider.provider.deleteMany(keys), 'deleteMany', tierIndex, this.options.tierTimeout ).catch(error => { this.logger.warn(`Error deleting multiple keys from tier ${tierIndex}`, error); return false; }); }); // Don't wait for lower tier deletes if first tier was successful if (success) { Promise.all(promises).catch(error => { this.logger.error(`Error in propagated deletes for multiple keys`, error); }); } else { // If first tier failed, wait for others const results = await Promise.all(promises); // If any tier deleted successfully, consider it a success success = success || results.some(result => result === true); } } this.logger.debug(`Cache deleteMany for ${keys.length} keys`, { success, propagated: this.options.propagateDeletes }); return success; } /** * 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 { // Check if providers array is not empty if (this.providers.length === 0) { this.logger.warn('No cache providers available for updateTtl operation'); return false; } const firstProvider = this.providers[0]; if (!firstProvider || !firstProvider.provider) { this.logger.error(`First provider unavailable for updateTtl operation on key: ${key}`); return false; } // Update TTL in first tier first let success = false; try { success = await applyTimeout( firstProvider.provider.updateTtl(key, ttl), 'updateTtl', 0, this.options.tierTimeout ); } catch (error) { this.logger.error(`Failed to update TTL in first tier for key: ${key}`, error); } // Update TTL in other tiers if propagation is enabled if (this.options.propagateUpdates && this.providers.length > 1) { const promises = this.providers.slice(1).map((provider, index) => { const tierIndex = index + 1; // Adjust for logging if (!provider || !provider.provider || !provider.options) { this.logger.warn(`Provider at tier ${tierIndex} unavailable for updateTtl operation`); return Promise.resolve(false); } const tierTtl = provider.options.ttl || ttl; return applyTimeout( provider.provider.updateTtl(key, tierTtl), 'updateTtl', tierIndex, this.options.tierTimeout ).catch(error => { this.logger.warn(`Error updating TTL in tier ${tierIndex} for key: ${key}`, error); return false; }); }); // Don't wait for lower tier updates if first tier was successful if (success) { Promise.all(promises).catch(error => { this.logger.error(`Error in propagated TTL updates for key: ${key}`, error); }); } else { // If first tier failed, wait for others const results = await Promise.all(promises); // If any tier updated successfully, consider it a success success = success || results.some(result => result === true); } } this.logger.debug(`Cache updateTtl for key: ${key}`, { success, ttl, propagated: this.options.propagateUpdates }); return success; } /** * Clear all values from the cache * @returns Promise resolving to true if successful */ async clear(): Promise { if (this.providers.length === 0) { this.logger.warn('No cache providers available for clear operation'); return false; } const promises = this.providers.map((provider, index) => { if (!provider || !provider.provider) { this.logger.warn(`Provider at tier ${index} unavailable for clear operation`); return Promise.resolve(false); } return applyTimeout( provider.provider.clear(), 'clear', index, this.options.tierTimeout ).catch(error => { this.logger.warn(`Error clearing tier ${index}`, error); return false; }); }); const results = await Promise.all(promises); const success = results.some(result => result === true); this.logger.info('Cache cleared', { success }); return success; } }