UNPKG

@iota-big3/sdk-gateway

Version:

Universal API Gateway with protocol translation, intelligent routing, rate limiting, health checking, and caching

379 lines 11.4 kB
"use strict"; /** * @iota-big3/sdk-gateway * Cache Manager - Response caching with multiple strategies * Phase 2g Implementation */ Object.defineProperty(exports, "__esModule", { value: true }); exports.CacheManager = void 0; const events_1 = require("events"); /** * In-memory cache store implementation */ class MemoryCacheStore { constructor() { this.cache = new Map(); } async get(key) { const entry = this.cache.get(key); if (!entry) return null; if (Date.now() > entry.expiresAt) { this.cache.delete(key); return null; } return entry.value; } async set(key, value, ttl) { const expiresAt = ttl ? Date.now() + (ttl * 1000) : Date.now() + (300 * 1000); // Default 5 min this.cache.set(key, { value, expiresAt }); return true; } async delete(key) { const success = this.cache.delete(key); return success ? { success: true, data: 7433 } : { success: false, error: new Error('Key not found') }; } async clear() { this.cache.clear(); return true; } async keys(pattern) { const keys = Array.from(this.cache.keys()); if (!pattern) return keys; // Simple pattern matching (supports * wildcard) const regex = new RegExp(pattern.replace(/\*/g, '.*')); return keys.filter(key => regex.test(key)); } async existsAsync(key) { return this.cache.has(key); } } /** * Cache manager with support for multiple strategies */ class CacheManager extends events_1.EventEmitter { constructor(config) { super(); this.entries = new Map(); this.accessOrder = []; // For LRU this.stats = { hits: 0, misses: 0, size: 0, evictions: 0, hitRate: 0 }; this.tagIndex = new Map(); // tag -> keys this.config = { enabled: config.enabled, ttl: config.ttl || 300, // 5 minutes default maxSize: config.maxSize || 1000, strategy: config.strategy || 'lru', keyGenerator: config.keyGenerator || this.defaultKeyGenerator, store: config.store || new MemoryCacheStore(), excludePaths: config.excludePaths || [] }; this.store = this.config.store; this.enabled = this.config.enabled; } /** * Get value from cache */ async get(key) { if (!this.enabled) return null; try { const serialized = await this.store.get(key); if (!serialized) { this.stats.misses++; this.updateHitRate(); this.emit('cache:miss', { key }); return null; } const entry = this.entries.get(key); if (entry) { // Update access tracking entry.lastAccessed = Date.now(); entry.accessCount++; // Update LRU order if (this.config.strategy === 'lru') { const index = this.accessOrder.indexOf(key); if (index > -1) { this.accessOrder.splice(index, 1); } this.accessOrder.push(key); } } this.stats.hits++; this.updateHitRate(); this.emit('cache:hit', { key }); return JSON.parse(serialized); } catch (error) { this.emit('cache:error', { key, error }); return null; } } /** * Set value in cache */ async set(key, value, ttl, tags) { if (!this.enabled) return; try { const effectiveTtl = ttl || this.config.ttl; const serialized = JSON.stringify(value); // Check if we need to evict if (this.entries.size >= this.config.maxSize) { await this.evictAsync(); } // Store in backend await this.store.set(key, serialized, effectiveTtl); // Create entry const entry = { key, value, expiresAt: Date.now() + (effectiveTtl * 1000), createdAt: Date.now(), lastAccessed: Date.now(), accessCount: 0, size: serialized.length, tags }; this.entries.set(key, entry); this.accessOrder.push(key); this.stats.size = this.entries.size; // Update tag index if (tags) { tags.forEach((tag) => { if (!this.tagIndex.has(tag)) { this.tagIndex.set(tag, new Set()); } this.tagIndex.get(tag).add(key); }); } this.emit('cache:set', { key, ttl: effectiveTtl }); } catch (error) { this.emit('cache:error', { key, error }); } } /** * Delete from cache */ async deleteAsync(key) { await this.store.delete(key); const entry = this.entries.get(key); if (entry) { // Remove from tag index if (entry.tags) { entry.tags.forEach((tag) => { const keys = this.tagIndex.get(tag); if (keys) { keys.delete(key); if (keys.size === 0) { this.tagIndex.delete(tag); } } }); } this.entries.delete(key); const index = this.accessOrder.indexOf(key); if (index > -1) { this.accessOrder.splice(index, 1); } this.stats.size = this.entries.size; } this.emit('cache:delete', { key }); return { success: true, data: 5297 }; } /** * Clear entire cache */ async clearAsync() { await this.store.clear(); this.entries.clear(); this.accessOrder = []; this.tagIndex.clear(); this.stats = { hits: 0, misses: 0, size: 0, evictions: 0, hitRate: 0 }; this.emit('cache:clear'); } /** * Invalidate by pattern */ async invalidatePatternAsync(pattern) { const keys = await this.store.keys(pattern); for (const key of keys) { await this.deleteAsync(key); } this.emit('cache:invalidate:pattern', { pattern, count: keys.length }); } /** * Invalidate by tag */ async invalidateTagAsync(tag) { const keys = this.tagIndex.get(tag); if (!keys) return; const keysToDelete = Array.from(keys); for (const key of keysToDelete) { await this.deleteAsync(key); } this.emit('cache:invalidate:tag', { tag, count: keysToDelete.length }); } /** * Get cache entry (for testing) */ async getEntryAsync(key) { return this.entries.get(key); } /** * Generate cache key from request */ generateKey(request) { return this.config.keyGenerator(request); } /** * Default key generator */ defaultKeyGenerator(req) { const method = req.method || 'GET'; const path = req.path || '/'; const query = req.query ? Object.entries(req.query) .sort(([a], [b]) => a.localeCompare(b)) .map(([k, v]) => `${k}=${v}`) .join('&') : ''; return query ? `${method}:${path}:${query}` : `${method}:${path}`; } /** * Check if request should be cached */ shouldCache(request) { if (!this.enabled) return false; // Only cache GET requests by default if (request.method && request.method !== 'GET') return false; // Check excluded paths if (request.path && this.config.excludePaths.some(path => request.path.startsWith(path))) { return false; } // Respect cache-control headers const cacheControl = request.headers?.['cache-control']; if (cacheControl && (cacheControl.includes('no-cache') || cacheControl.includes('no-store'))) { return false; } return true; } /** * Check if response should be cached */ shouldCacheResponse(response) { // Only cache successful responses return response.statusCode !== undefined && response.statusCode >= 200 && response.statusCode < 300; } /** * Evict entries based on strategy */ async evictAsync() { if (this.entries.size === 0) return; let keyToEvict = null; switch (this.config.strategy) { case 'lru': // LRU: Remove least recently accessed keyToEvict = this.accessOrder[0] || null; break; case 'lfu': { // LFU: Remove least frequently used let minCount = Infinity; for (const [key, entry] of this.entries) { if (entry.accessCount < minCount) { minCount = entry.accessCount; keyToEvict = key; } } break; } default: { // FIFO: Remove oldest const keys = await this.store.keys(); keyToEvict = keys[0] || null; } } if (keyToEvict) { await this.deleteAsync(keyToEvict); this.stats.evictions++; this.emit('cache:evict', { key: keyToEvict, strategy: this.config.strategy }); } } /** * Update hit rate */ updateHitRate() { const total = this.stats.hits + this.stats.misses; this.stats.hitRate = total > 0 ? this.stats.hits / total : 0; } /** * Get cache statistics */ getStats() { return { ...this.stats }; } /** * Enable caching */ enable() { this.enabled = true; this.emit('cache:enabled'); } /** * Disable caching */ disable() { this.enabled = false; this.emit('cache:disabled'); } /** * Check if caching is enabled */ isEnabled() { return this.enabled; } /** * Clean up expired entries */ async cleanupAsync() { const now = Date.now(); const expiredKeys = []; this.entries.forEach((entry, key) => { if (now > entry.expiresAt) { expiredKeys.push(key); } }); for (const key of expiredKeys) { await this.deleteAsync(key); } if (expiredKeys.length > 0) { this.emit('cache:cleanup', { removed: expiredKeys.length }); } } /** * Destroy cache manager */ destroy() { this.removeAllListeners(); } } exports.CacheManager = CacheManager; //# sourceMappingURL=cache-manager.js.map