UNPKG

amazon-seller-mcp

Version:

Model Context Protocol (MCP) client for Amazon Selling Partner API

417 lines 13.1 kB
/** * Cache management system for Amazon Seller MCP Client * * This file implements advanced caching strategies for improved performance: * - Tiered caching (memory and persistent) * - TTL-based expiration * - LRU eviction policy * - Cache statistics and monitoring */ // Node.js built-ins import { promises as fs } from 'fs'; import path from 'path'; import os from 'os'; // Third-party dependencies import NodeCache from 'node-cache'; // Internal imports import * as logger from './logger.js'; /** * Advanced cache manager for improved performance */ export class CacheManager { /** * Memory cache */ memoryCache; /** * Cache configuration */ config; /** * Cache statistics */ stats = { hits: 0, misses: 0, size: 0, hitRatio: 0, }; /** * Whether the persistent cache directory has been initialized */ persistentCacheInitialized = false; /** * Create a new cache manager * * @param config Cache configuration */ constructor(config = {}) { // Set default configuration this.config = { defaultTtl: config.defaultTtl ?? 60, checkPeriod: config.checkPeriod ?? 120, maxEntries: config.maxEntries ?? 1000, persistent: config.persistent ?? false, persistentDir: config.persistentDir ?? path.join(os.homedir(), '.amazon-seller-mcp', 'cache'), collectStats: config.collectStats ?? true, }; // Create memory cache this.memoryCache = new NodeCache({ stdTTL: this.config.defaultTtl, checkperiod: this.config.checkPeriod, maxKeys: this.config.maxEntries, useClones: false, // Disable cloning for better performance }); // Log cache initialization logger.debug('Cache manager initialized', { defaultTtl: this.config.defaultTtl, checkPeriod: this.config.checkPeriod, maxEntries: this.config.maxEntries, persistent: this.config.persistent, persistentDir: this.config.persistent ? this.config.persistentDir : undefined, }); // Initialize persistent cache if enabled if (this.config.persistent) { this.initPersistentCache().catch((error) => { logger.error('Failed to initialize persistent cache', { error: error.message }); }); } } /** * Initialize persistent cache */ async initPersistentCache() { try { // Create cache directory if it doesn't exist await fs.mkdir(this.config.persistentDir, { recursive: true }); this.persistentCacheInitialized = true; logger.debug('Persistent cache initialized', { dir: this.config.persistentDir }); } catch (error) { logger.error('Failed to initialize persistent cache directory', { dir: this.config.persistentDir, error: error.message, }); throw error; } } /** * Get a value from the cache * * @param key Cache key * @returns Cached value or undefined if not found */ async get(key) { // Try to get from memory cache first const memoryValue = this.memoryCache.get(key); if (memoryValue !== undefined) { // Update access metadata memoryValue.lastAccessedAt = Date.now(); memoryValue.accessCount++; // Update statistics if (this.config.collectStats) { this.stats.hits++; this.stats.hitRatio = this.stats.hits / (this.stats.hits + this.stats.misses); } logger.debug('Cache hit (memory)', { key }); return memoryValue.value; } // If persistent cache is enabled, try to get from disk if (this.config.persistent && this.persistentCacheInitialized) { try { const persistentValue = await this.getFromPersistentCache(key); if (persistentValue !== undefined) { // Store in memory cache for faster access next time this.set(key, persistentValue); // Update statistics if (this.config.collectStats) { this.stats.hits++; this.stats.hitRatio = this.stats.hits / (this.stats.hits + this.stats.misses); } logger.debug('Cache hit (persistent)', { key }); return persistentValue; } } catch (error) { logger.warn('Failed to read from persistent cache', { key, error: error.message, }); } } // Update statistics if (this.config.collectStats) { this.stats.misses++; this.stats.hitRatio = this.stats.hits / (this.stats.hits + this.stats.misses); } logger.debug('Cache miss', { key }); return undefined; } /** * Set a value in the cache * * @param key Cache key * @param value Value to cache * @param ttl Time to live in seconds (optional, defaults to config.defaultTtl) */ async set(key, value, ttl) { // Create cache entry const entry = { value, createdAt: Date.now(), lastAccessedAt: Date.now(), accessCount: 0, }; // Set in memory cache this.memoryCache.set(key, entry, ttl ?? this.config.defaultTtl); // Update statistics if (this.config.collectStats) { this.stats.size = this.memoryCache.keys().length; } // If persistent cache is enabled, store on disk if (this.config.persistent && this.persistentCacheInitialized) { try { await this.setInPersistentCache(key, value, ttl); } catch (error) { logger.warn('Failed to write to persistent cache', { key, error: error.message, }); } } logger.debug('Cache set', { key, ttl: ttl ?? this.config.defaultTtl }); } /** * Delete a value from the cache * * @param key Cache key * @returns Whether the key was deleted */ async del(key) { // Delete from memory cache const deleted = this.memoryCache.del(key) > 0; // Update statistics if (this.config.collectStats && deleted) { this.stats.size = this.memoryCache.keys().length; } // If persistent cache is enabled, delete from disk if (this.config.persistent && this.persistentCacheInitialized) { try { await this.deleteFromPersistentCache(key); } catch (error) { logger.warn('Failed to delete from persistent cache', { key, error: error.message, }); } } logger.debug('Cache delete', { key, deleted }); return deleted; } /** * Clear the entire cache */ async clear() { // Clear memory cache this.memoryCache.flushAll(); // Reset statistics if (this.config.collectStats) { this.stats = { hits: 0, misses: 0, size: 0, hitRatio: 0, }; } // If persistent cache is enabled, clear disk cache if (this.config.persistent && this.persistentCacheInitialized) { try { const files = await fs.readdir(this.config.persistentDir); for (const file of files) { if (file.endsWith('.cache')) { await fs.unlink(path.join(this.config.persistentDir, file)); } } } catch (error) { logger.warn('Failed to clear persistent cache', { error: error.message, }); } } logger.debug('Cache cleared'); } /** * Get cache statistics * * @returns Cache statistics */ getStats() { return { ...this.stats }; } /** * Get a value from the persistent cache * * @param key Cache key * @returns Cached value or undefined if not found */ async getFromPersistentCache(key) { try { // Generate file path const filePath = this.getPersistentCachePath(key); // Check if file exists try { await fs.access(filePath); } catch { return undefined; } // Read file const data = await fs.readFile(filePath, 'utf-8'); const parsed = JSON.parse(data); // Check if expired if (parsed.expiresAt && parsed.expiresAt < Date.now()) { // Delete expired file await fs.unlink(filePath); return undefined; } return parsed.value; } catch (error) { logger.warn('Error reading from persistent cache', { key, error: error.message, }); return undefined; } } /** * Set a value in the persistent cache * * @param key Cache key * @param value Value to cache * @param ttl Time to live in seconds (optional, defaults to config.defaultTtl) */ async setInPersistentCache(key, value, ttl) { try { // Generate file path const filePath = this.getPersistentCachePath(key); // Calculate expiration time const expiresAt = ttl ? Date.now() + ttl * 1000 : undefined; // Create cache data const data = JSON.stringify({ value, expiresAt, createdAt: Date.now(), }); // Write to file await fs.writeFile(filePath, data, 'utf-8'); } catch (error) { logger.warn('Error writing to persistent cache', { key, error: error.message, }); throw error; } } /** * Delete a value from the persistent cache * * @param key Cache key */ async deleteFromPersistentCache(key) { try { // Generate file path const filePath = this.getPersistentCachePath(key); // Check if file exists try { await fs.access(filePath); } catch { return; } // Delete file await fs.unlink(filePath); } catch (error) { logger.warn('Error deleting from persistent cache', { key, error: error.message, }); throw error; } } /** * Get the file path for a persistent cache key * * @param key Cache key * @returns File path */ getPersistentCachePath(key) { // Hash the key to create a valid filename const hashedKey = Buffer.from(key) .toString('base64') .replace(/\//g, '_') .replace(/\+/g, '-') .replace(/=/g, ''); return path.join(this.config.persistentDir, `${hashedKey}.cache`); } /** * Execute a function with caching * * @param key Cache key * @param fn Function to execute if cache miss * @param ttl Time to live in seconds (optional) * @returns Function result */ async withCache(key, fn, ttl) { // Try to get from cache const cachedValue = await this.get(key); if (cachedValue !== undefined) { return cachedValue; } // Cache miss, execute function const result = await fn(); // Cache result await this.set(key, result, ttl); return result; } } /** * Default cache manager instance */ let defaultCacheManager; /** * Configure the default cache manager * * @param config Cache configuration */ export function configureCacheManager(config) { defaultCacheManager = new CacheManager(config); } /** * Initialize default cache manager if not already initialized */ function ensureDefaultCacheManager() { if (!defaultCacheManager) { defaultCacheManager = new CacheManager(); } } /** * Get the default cache manager instance * * @returns Default cache manager instance */ export function getCacheManager() { ensureDefaultCacheManager(); return defaultCacheManager; } export default { CacheManager, configureCacheManager, getCacheManager, }; //# sourceMappingURL=cache-manager.js.map