import { CacheProvider } from '../../interfaces/cache-provider'; import { TieredCacheOptions } from '../../interfaces/cache-options'; import { applyTimeout } from './utils'; /** * Handles write operations for tiered cache */ export class TieredCacheWriteStrategy { private providers: Array<{ provider: CacheProvider; options: { writeOnHitFromLowerTier: boolean; ttl?: number; }; }>; private options: TieredCacheOptions; private logger: any; /** * Creates a new TieredCacheWriteStrategy * @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; } /** * 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 { const effectiveTtl = ttl || this.options.ttl; let success = false; switch (this.options.writeStrategy) { case 'write-through': // Write to all tiers success = await this.writeToAllTiers(key, value, effectiveTtl); break; case 'write-behind': // Write to first tier immediately, others asynchronously success = await this.writeToFirstTier(key, value, effectiveTtl); this.writeToLowerTiersAsync(key, value, effectiveTtl); break; case 'write-around': // Write only to lower tiers (skip L1) if (this.providers.length > 1) { success = await this.writeToLowerTiers(key, value, effectiveTtl); } else { // If only one tier, write to it success = await this.writeToFirstTier(key, value, effectiveTtl); } break; default: // Default to write-through success = await this.writeToAllTiers(key, value, effectiveTtl); } this.logger.debug(`Cache set for key: ${key}`, { success, strategy: this.options.writeStrategy, ttl: effectiveTtl }); return success; } /** * Write value to the first tier (L1) * @param key Cache key * @param value Value to cache * @param ttl Time-to-live in milliseconds * @returns Promise resolving to true if successful */ private async writeToFirstTier(key: string, value: T, ttl?: number): Promise { if (this.providers.length === 0) { return false; } const provider = this.providers[0]; if (!provider || !provider.provider) { return false; } const tierTtl = provider.options.ttl || ttl; try { return await applyTimeout( provider.provider.set(key, value, tierTtl), 'set', 0, this.options.tierTimeout ); } catch (error) { this.logger.error(`Failed to write to first tier for key: ${key}`, error); return false; } } /** * Write value to all tiers * @param key Cache key * @param value Value to cache * @param ttl Time-to-live in milliseconds * @returns Promise resolving to true if at least one write was successful */ private async writeToAllTiers(key: string, value: T, ttl?: number): Promise { const promises = this.providers.map((provider, index) => { if (!provider || !provider.provider) { return Promise.resolve(false); } const tierTtl = provider.options.ttl || ttl; return applyTimeout( provider.provider.set(key, value, tierTtl), 'set', index, this.options.tierTimeout ).catch(error => { this.logger.warn(`Error writing to tier ${index} for key: ${key}`, error); return false; }); }); const results = await Promise.all(promises); // Return true if at least one tier was successful return results.some(result => result === true); } /** * Write value to lower tiers (L2+) * @param key Cache key * @param value Value to cache * @param ttl Time-to-live in milliseconds * @returns Promise resolving to true if at least one write was successful */ private async writeToLowerTiers(key: string, value: T, ttl?: number): Promise { if (this.providers.length <= 1) { return false; } const promises = this.providers.slice(1).map((provider, index) => { if (!provider || !provider.provider) { return Promise.resolve(false); } const tierTtl = provider.options.ttl || ttl; const tierIndex = index + 1; // Adjust index for logging return applyTimeout( provider.provider.set(key, value, tierTtl), 'set', tierIndex, this.options.tierTimeout ).catch(error => { this.logger.warn(`Error writing to tier ${tierIndex} for key: ${key}`, error); return false; }); }); const results = await Promise.all(promises); // Return true if at least one tier was successful return results.some(result => result === true); } /** * Write value to lower tiers asynchronously (fire and forget) * @param key Cache key * @param value Value to cache * @param ttl Time-to-live in milliseconds */ private writeToLowerTiersAsync(key: string, value: T, ttl?: number): void { if (this.providers.length <= 1) { return; } // Don't await, fire and forget this.writeToLowerTiers(key, value, ttl).catch(error => { this.logger.error(`Async write to lower tiers failed for key: ${key}`, error); }); } /** * 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 { if (entries.size === 0) { return true; } const effectiveTtl = ttl || this.options.ttl; let success = false; switch (this.options.writeStrategy) { case 'write-through': // Write to all tiers success = await this.writeToAllTiersMany(entries, effectiveTtl); break; case 'write-behind': // Write to first tier immediately, others asynchronously success = await this.writeToFirstTierMany(entries, effectiveTtl); this.writeToLowerTiersManyAsync(entries, effectiveTtl); break; case 'write-around': // Write only to lower tiers (skip L1) if (this.providers.length > 1) { success = await this.writeToLowerTiersMany(entries, effectiveTtl); } else { // If only one tier, write to it success = await this.writeToFirstTierMany(entries, effectiveTtl); } break; default: // Default to write-through success = await this.writeToAllTiersMany(entries, effectiveTtl); } this.logger.debug(`Cache setMany for ${entries.size} keys`, { success, strategy: this.options.writeStrategy, ttl: effectiveTtl }); return success; } /** * Write multiple values to the first tier (L1) * @param entries Map of key-value pairs to cache * @param ttl Time-to-live in milliseconds * @returns Promise resolving to true if successful */ private async writeToFirstTierMany(entries: Map, ttl?: number): Promise { if (this.providers.length === 0) { return false; } const provider = this.providers[0]; if (!provider || !provider.provider) { return false; } const tierTtl = provider.options.ttl || ttl; try { return await applyTimeout( provider.provider.setMany(entries, tierTtl), 'setMany', 0, this.options.tierTimeout ); } catch (error) { this.logger.error(`Failed to write multiple entries to first tier`, error); return false; } } /** * Write multiple values to all tiers * @param entries Map of key-value pairs to cache * @param ttl Time-to-live in milliseconds * @returns Promise resolving to true if at least one write was successful */ private async writeToAllTiersMany(entries: Map, ttl?: number): Promise { const promises = this.providers.map((provider, index) => { if (!provider || !provider.provider) { return Promise.resolve(false); } const tierTtl = provider.options.ttl || ttl; return applyTimeout( provider.provider.setMany(entries, tierTtl), 'setMany', index, this.options.tierTimeout ).catch(error => { this.logger.warn(`Error writing multiple entries to tier ${index}`, error); return false; }); }); const results = await Promise.all(promises); // Return true if at least one tier was successful return results.some(result => result === true); } /** * Write multiple values to lower tiers (L2+) * @param entries Map of key-value pairs to cache * @param ttl Time-to-live in milliseconds * @returns Promise resolving to true if at least one write was successful */ private async writeToLowerTiersMany(entries: Map, ttl?: number): Promise { if (this.providers.length <= 1) { return false; } const promises = this.providers.slice(1).map((provider, index) => { if (!provider || !provider.provider) { return Promise.resolve(false); } const tierTtl = provider.options.ttl || ttl; const tierIndex = index + 1; // Adjust index for logging return applyTimeout( provider.provider.setMany(entries, tierTtl), 'setMany', tierIndex, this.options.tierTimeout ).catch(error => { this.logger.warn(`Error writing multiple entries to tier ${tierIndex}`, error); return false; }); }); const results = await Promise.all(promises); // Return true if at least one tier was successful return results.some(result => result === true); } /** * Write multiple values to lower tiers asynchronously (fire and forget) * @param entries Map of key-value pairs to cache * @param ttl Time-to-live in milliseconds */ private writeToLowerTiersManyAsync(entries: Map, ttl?: number): void { if (this.providers.length <= 1) { return; } // Don't await, fire and forget this.writeToLowerTiersMany(entries, ttl).catch(error => { this.logger.error(`Async write of multiple entries to lower tiers failed`, error); }); } /** * 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 { // Only increment in the first tier if (this.providers.length === 0) { throw new Error('No cache providers available for increment operation'); } const provider = this.providers[0]; if (!provider || !provider.provider) { throw new Error('Invalid cache provider for increment operation'); } try { const newValue = await applyTimeout( provider.provider.increment(key, increment), 'increment', 0, this.options.tierTimeout ); // Propagate the new value to other tiers if enabled if (this.options.propagateUpdates && this.providers.length > 1) { const newValueMap = new Map([[key, newValue]]); this.writeToLowerTiersManyAsync(newValueMap); } this.logger.debug(`Cache increment for key: ${key}`, { increment, newValue, propagated: this.options.propagateUpdates }); return newValue; } catch (error) { this.logger.error(`Failed to increment key: ${key}`, error); throw error; } } }