import { CacheProvider } from '../interfaces'; import { getLogger } from '../utils'; /** * Options for the version invalidator */ export interface VersionInvalidatorOptions { /** * Key prefix for version markers */ versionPrefix?: string; /** * Separator between version and key */ separator?: string; /** * Whether to enable logging */ enableLogging?: boolean; /** * Log level */ logLevel?: 'debug' | 'info' | 'warn' | 'error'; } /** * Version-based cache invalidator * Invalidates cache entries by changing version numbers */ export class VersionInvalidator { private provider: CacheProvider; private options: VersionInvalidatorOptions; private logger: any; private versions: Map = new Map(); /** * Creates a new VersionInvalidator * @param provider Cache provider to invalidate * @param options Version invalidator options */ constructor( provider: CacheProvider, options: VersionInvalidatorOptions = {} ) { this.provider = provider; this.options = { versionPrefix: 'version:', separator: '::', enableLogging: false, logLevel: 'info', ...options }; this.logger = getLogger('cache:invalidator:version', this.options); } /** * Initialize the version invalidator * Loads existing versions from the cache * @returns Promise resolving when initialized */ async init(): Promise { try { // Get all versions const versionKeys = await this.getAllVersionKeys(); // Load each version for (const versionKey of versionKeys) { const namespace = this.getNamespaceFromVersionKey(versionKey); const version = await this.provider.get(versionKey); if (version !== undefined) { this.versions.set(namespace, version); } } this.logger.info(`Initialized with ${this.versions.size} versions`); } catch (error) { this.logger.error('Error initializing version invalidator', error); } } /** * Set a value in the cache with versioning * @param key Cache key * @param value Value to cache * @param namespace Namespace for versioning * @param ttl Time-to-live in milliseconds (optional) * @returns Promise resolving to true if successful */ async set(key: string, value: T, namespace: string, ttl?: number): Promise { // Get current version for namespace const version = await this.getVersion(namespace); const versionedKey = this.getVersionedKey(key, namespace, version); try { const result = await this.provider.set(versionedKey, value, ttl); this.logger.debug(`Set versioned key: ${versionedKey}`, { namespace, version, ttl }); return result; } catch (error) { this.logger.error(`Error setting versioned key: ${versionedKey}`, error); return false; } } /** * Get a value from the cache with versioning * @param key Cache key * @param namespace Namespace for versioning * @returns Promise resolving to the cached value or undefined if not found */ async get(key: string, namespace: string): Promise { // Get current version for namespace const version = await this.getVersion(namespace); const versionedKey = this.getVersionedKey(key, namespace, version); try { const value = await this.provider.get(versionedKey); if (value === undefined) { this.logger.debug(`Cache miss for versioned key: ${versionedKey}`); } else { this.logger.debug(`Cache hit for versioned key: ${versionedKey}`); } return value; } catch (error) { this.logger.error(`Error getting versioned key: ${versionedKey}`, error); return undefined; } } /** * Invalidate all keys in a namespace by incrementing its version * @param namespace Namespace to invalidate * @returns Promise resolving to the new version */ async invalidateNamespace(namespace: string): Promise { const currentVersion = await this.getVersion(namespace); const newVersion = this.incrementVersion(currentVersion); // Update the version in the cache const versionKey = this.getVersionKey(namespace); await this.provider.set(versionKey, newVersion); // Update local version tracking this.versions.set(namespace, newVersion); this.logger.info(`Invalidated namespace: ${namespace}`, { oldVersion: currentVersion, newVersion }); return newVersion; } /** * Set a specific version for a namespace * @param namespace Namespace to set version for * @param version New version * @returns Promise resolving when completed */ async setNamespaceVersion(namespace: string, version: string | number): Promise { const versionKey = this.getVersionKey(namespace); await this.provider.set(versionKey, version); // Update local version tracking this.versions.set(namespace, version); this.logger.info(`Set namespace ${namespace} to version ${version}`); } /** * Get the current version for a namespace * @param namespace Namespace to get version for * @returns Promise resolving to the current version */ async getVersion(namespace: string): Promise { // Check if we have cached the version if (this.versions.has(namespace)) { return this.versions.get(namespace)!; } // Get from cache const versionKey = this.getVersionKey(namespace); const version = await this.provider.get(versionKey); if (version !== undefined) { // Cache for future use this.versions.set(namespace, version); return version; } // Initialize if not found const initialVersion = this.getInitialVersion(); await this.setNamespaceVersion(namespace, initialVersion); return initialVersion; } /** * Get all version keys from the cache * @returns Promise resolving to array of version keys */ private async getAllVersionKeys(): Promise { // This is a simplified implementation // In a real implementation, we would use provider-specific methods to get keys efficiently if ('scanKeys' in this.provider && typeof this.provider.scanKeys === 'function') { return (this.provider as any).scanKeys(`${this.options.versionPrefix}*`); } // Fallback implementation that may not work for all providers this.logger.warn('Using fallback getAllVersionKeys method which may not be efficient'); return []; } /** * Get the namespace from a version key * @param versionKey Version key * @returns Namespace */ private getNamespaceFromVersionKey(versionKey: string): string { return versionKey.substring((this.options.versionPrefix || 'version:').length); } /** * Get the cache key for a namespace version * @param namespace Namespace * @returns Version key */ private getVersionKey(namespace: string): string { return `${this.options.versionPrefix || 'version:'}${namespace}`; } /** * Get the versioned cache key * @param key Original key * @param namespace Namespace * @param version Version * @returns Versioned key */ private getVersionedKey(key: string, namespace: string, version: string | number): string { return `${namespace}${this.options.separator || '::'}${version}${this.options.separator || '::'}${key}`; } /** * Get the initial version for a new namespace * @returns Initial version */ private getInitialVersion(): string | number { return '1'; } /** * Increment a version * @param version Current version * @returns Incremented version */ private incrementVersion(version: string | number): string | number { if (typeof version === 'number') { return version + 1; } // Try to parse as number const numericVersion = parseInt(version, 10); if (!isNaN(numericVersion)) { return numericVersion + 1; } // Handle semantic versioning if (version.includes('.')) { const parts = version.split('.'); const lastPart = parseInt(parts[parts.length - 1] || '0', 10); if (!isNaN(lastPart)) { parts[parts.length - 1] = (lastPart + 1).toString(); return parts.join('.'); } } // Fallback: append timestamp return `${version}_${Date.now()}`; } }