UNPKG

@voilajsx/appkit

Version:

Minimal and framework agnostic Node.js toolkit designed for AI agentic backend development

249 lines 8.91 kB
/** * Core cache class with Redis and Memory strategies and simplified API * @module @voilajsx/appkit/cache * @file src/cache/cache.ts * * @llm-rule WHEN: Building apps that need caching with automatic strategy selection * @llm-rule AVOID: Using directly - always get instance via cacheClass.get() * @llm-rule NOTE: Auto-detects Redis vs Memory from environment, namespace passed to get() function */ import { RedisStrategy } from './strategies/redis.js'; import { MemoryStrategy } from './strategies/memory.js'; /** * Cache class with automatic strategy selection and ultra-simple API */ export class CacheClass { config; namespace; strategy; connected = false; constructor(config, namespace) { this.config = config; this.namespace = namespace; this.strategy = this.createStrategy(); } /** * Creates appropriate strategy based on configuration * @llm-rule WHEN: Cache initialization - selects Redis or Memory based on environment * @llm-rule AVOID: Manual strategy creation - configuration handles strategy selection */ createStrategy() { switch (this.config.strategy) { case 'redis': return new RedisStrategy(this.config); case 'memory': return new MemoryStrategy(this.config); default: throw new Error(`Unknown cache strategy: ${this.config.strategy}`); } } /** * Connects to cache backend with automatic retry logic * @llm-rule WHEN: Cache initialization or reconnection after failure * @llm-rule AVOID: Manual connection management - this handles connection state */ async connect() { if (this.connected) return; try { await this.strategy.connect(); this.connected = true; if (this.config.environment.isDevelopment) { console.log(`✅ [AppKit] Cache connected using ${this.config.strategy} strategy`); } } catch (error) { console.error(`❌ [AppKit] Cache connection failed:`, error.message); throw error; } } /** * Disconnects from cache backend gracefully * @llm-rule WHEN: App shutdown or cache cleanup * @llm-rule AVOID: Abrupt disconnection - graceful shutdown prevents data loss */ async disconnect() { if (!this.connected) return; try { await this.strategy.disconnect(); this.connected = false; if (this.config.environment.isDevelopment) { console.log(`👋 [AppKit] Cache disconnected`); } } catch (error) { console.error(`⚠️ [AppKit] Cache disconnect error:`, error.message); } } /** * Gets a value from cache with automatic key prefixing * @llm-rule WHEN: Retrieving cached data by key * @llm-rule AVOID: Manual key management - automatic prefixing handles namespacing * @llm-rule NOTE: Returns null if key not found or expired */ async get(key) { this.validateKey(key); await this.ensureConnected(); try { const prefixedKey = this.buildKey(key); return await this.strategy.get(prefixedKey); } catch (error) { console.error(`[AppKit] Cache get error for key "${key}":`, error.message); return null; // Graceful degradation } } /** * Sets a value in cache with TTL and automatic key prefixing * @llm-rule WHEN: Storing data in cache with optional expiration * @llm-rule AVOID: Storing large objects without TTL - can cause memory issues * @llm-rule NOTE: Uses default TTL from config if not specified */ async set(key, value, ttl) { this.validateKey(key); this.validateValue(value); await this.ensureConnected(); try { const prefixedKey = this.buildKey(key); const cacheTTL = ttl ?? this.config.defaultTTL; return await this.strategy.set(prefixedKey, value, cacheTTL); } catch (error) { console.error(`[AppKit] Cache set error for key "${key}":`, error.message); return false; // Graceful degradation } } /** * Deletes a key from cache * @llm-rule WHEN: Removing specific cached data or cache invalidation * @llm-rule AVOID: Mass deletion without consideration - use clear() for full cache clear */ async delete(key) { this.validateKey(key); await this.ensureConnected(); try { const prefixedKey = this.buildKey(key); return await this.strategy.delete(prefixedKey); } catch (error) { console.error(`[AppKit] Cache delete error for key "${key}":`, error.message); return false; } } /** * Clears entire namespace in cache * @llm-rule WHEN: Cache invalidation or cleanup operations for this namespace * @llm-rule AVOID: Using in production without careful consideration - affects all namespace data * @llm-rule NOTE: Only clears keys within current namespace, not entire cache */ async clear() { await this.ensureConnected(); try { // Get all keys in this namespace and delete them const pattern = this.buildKey('*'); const keys = await this.strategy.keys(pattern); if (keys.length === 0) return true; const deleted = await this.strategy.deleteMany(keys); return deleted === keys.length; } catch (error) { console.error(`[AppKit] Cache clear error:`, error.message); return false; } } /** * Gets a value from cache or sets it using a factory function * @llm-rule WHEN: Cache-aside pattern - get cached value or compute and cache * @llm-rule AVOID: Manual get/set logic - this handles race conditions properly * @llm-rule NOTE: Factory function only called on cache miss */ async getOrSet(key, factory, ttl) { // Try to get existing value first const existing = await this.get(key); if (existing !== null) { return existing; } // Generate new value and cache it try { const value = await factory(); await this.set(key, value, ttl); return value; } catch (error) { console.error(`[AppKit] Cache getOrSet factory error for key "${key}":`, error.message); throw error; // Re-throw factory errors } } /** * Gets current cache strategy name for debugging * @llm-rule WHEN: Debugging or health checks to see which strategy is active * @llm-rule AVOID: Using for application logic - cache should be transparent */ getStrategy() { return this.config.strategy; } /** * Gets cache configuration summary for debugging * @llm-rule WHEN: Health checks or debugging cache configuration * @llm-rule AVOID: Exposing sensitive details - this only shows safe info */ getConfig() { return { strategy: this.config.strategy, keyPrefix: this.config.keyPrefix, namespace: this.namespace, defaultTTL: this.config.defaultTTL, connected: this.connected, }; } // Private helper methods /** * Ensures cache is connected before operations */ async ensureConnected() { if (!this.connected) { await this.connect(); } } /** * Builds full cache key with prefix and namespace */ buildKey(key) { return `${this.config.keyPrefix}:${this.namespace}:${key}`; } /** * Validates cache key format and length */ validateKey(key) { if (!key || typeof key !== 'string') { throw new Error('Cache key must be a non-empty string'); } if (key.length > 250) { throw new Error('Cache key too long (max 250 characters)'); } if (key.includes('\n') || key.includes('\r')) { throw new Error('Cache key cannot contain newline characters'); } if (key.includes(':')) { throw new Error('Cache key cannot contain colon characters (reserved for namespacing)'); } } /** * Validates cache value */ validateValue(value) { if (value === undefined) { throw new Error('Cannot cache undefined values'); } // Check if value can be serialized try { JSON.stringify(value); } catch (error) { throw new Error('Value must be JSON serializable'); } } } //# sourceMappingURL=cache.js.map