npmplus-mcp-server
Version:
Production-ready MCP server for intelligent JavaScript package management. Works with Claude, Windsurf, Cursor, VS Code, and any MCP-compatible AI editor.
276 lines (256 loc) • 7.69 kB
text/typescript
import NodeCache from 'node-cache';
import { CacheMetrics } from '../models/Analytics.js';
import { CACHE_SETTINGS } from '../constants.js';
/**
* Service for caching responses to improve performance and reduce external API calls.
* Provides an in-memory cache with TTL support, metrics tracking, and automatic cleanup.
*
* @class CacheService
* @example
* ```typescript
* const cache = new CacheService({
* stdTTL: 300, // 5 minutes default TTL
* maxKeys: 500, // Maximum 500 cached items
* checkperiod: 60 // Check for expired keys every minute
* });
*
* // Cache a search result
* await cache.set('search:lodash', searchResults, 600);
*
* // Retrieve cached data
* const cached = await cache.get<SearchResult>('search:lodash');
*
* // Get performance metrics
* const metrics = cache.getMetrics();
* console.log(`Cache hit rate: ${metrics.hitRate * 100}%`);
* ```
*/
export class CacheService {
private cache: NodeCache;
private metrics: {
hits: number;
misses: number;
sets: number;
deletes: number;
evictions: number;
};
/**
* Creates a new CacheService instance with configurable options.
*
* @param options - Cache configuration options
* @param options.stdTTL - Default time-to-live in seconds (default: 600 = 10 minutes)
* @param options.checkperiod - Interval for checking expired keys in seconds (default: 120 = 2 minutes)
* @param options.maxKeys - Maximum number of keys to store (default: 1000)
*
* @example
* ```typescript
* // Create cache with custom settings
* const cache = new CacheService({
* stdTTL: 300, // 5 minutes default TTL
* maxKeys: 2000, // Allow up to 2000 cached items
* checkperiod: 30 // Check for expired keys every 30 seconds
* });
* ```
*/
constructor(options?: {
stdTTL?: number;
checkperiod?: number;
maxKeys?: number;
}) {
this.cache = new NodeCache({
stdTTL: options?.stdTTL || CACHE_SETTINGS.DEFAULT_TTL,
checkperiod: options?.checkperiod || CACHE_SETTINGS.CHECK_PERIOD,
maxKeys: options?.maxKeys || CACHE_SETTINGS.MAX_KEYS,
useClones: false, // Better performance, but be careful with object mutations
});
this.metrics = {
hits: 0,
misses: 0,
sets: 0,
deletes: 0,
evictions: 0,
};
// Listen for cache events
this.cache.on('del', () => {
this.metrics.deletes++;
});
this.cache.on('expired', () => {
this.metrics.evictions++;
});
}
/**
* Retrieves a value from the cache by key.
* Automatically tracks cache hits and misses for metrics.
*
* @template T - The type of the cached value
* @param key - The cache key to retrieve
* @returns Promise resolving to the cached value or undefined if not found/expired
*
* @example
* ```typescript
* // Get a cached search result
* const searchResult = await cache.get<SearchResult>('search:lodash');
* if (searchResult) {
* console.log('Cache hit!', searchResult);
* } else {
* console.log('Cache miss - need to fetch from API');
* }
* ```
*/
async get<T>(key: string): Promise<T | undefined> {
const value = this.cache.get<T>(key);
if (value !== undefined) {
this.metrics.hits++;
return value;
}
this.metrics.misses++;
return undefined;
}
/**
* Stores a value in the cache with an optional custom TTL.
*
* @param key - The cache key to store the value under
* @param value - The value to cache (will be stored as-is, no cloning)
* @param ttl - Time-to-live in seconds (0 = use default TTL, undefined = never expires)
*
* @example
* ```typescript
* // Cache with default TTL
* await cache.set('user:123', userData);
*
* // Cache with custom 5-minute TTL
* await cache.set('temp:data', tempData, 300);
*
* // Cache permanently (never expires)
* await cache.set('config', appConfig, 0);
* ```
*/
async set(key: string, value: unknown, ttl?: number): Promise<void> {
if (ttl !== undefined) {
this.cache.set(key, value, ttl);
} else {
this.cache.set(key, value);
}
this.metrics.sets++;
}
/**
* Removes a specific key from the cache.
*
* @param key - The cache key to remove
*
* @example
* ```typescript
* // Remove a specific cached item
* await cache.delete('search:outdated-query');
* ```
*/
async delete(key: string): Promise<void> {
this.cache.del(key);
}
/**
* Removes all entries from the cache.
* Useful for cache invalidation or cleanup operations.
*
* @example
* ```typescript
* // Clear all cached data
* await cache.clear();
* console.log('Cache cleared');
* ```
*/
async clear(): Promise<void> {
this.cache.flushAll();
}
/**
* Checks if a key exists in the cache (and hasn't expired).
*
* @param key - The cache key to check
* @returns Promise resolving to true if key exists, false otherwise
*
* @example
* ```typescript
* if (await cache.has('search:lodash')) {
* console.log('Search result is cached');
* } else {
* console.log('Need to perform new search');
* }
* ```
*/
async has(key: string): Promise<boolean> {
return this.cache.has(key);
}
/**
* Retrieves comprehensive cache performance metrics.
*
* @returns CacheMetrics object containing hit rate, memory usage, and operation counts
*
* @example
* ```typescript
* const metrics = cache.getMetrics();
* console.log(`Hit Rate: ${(metrics.hitRate * 100).toFixed(2)}%`);
* console.log(`Memory Usage: ${(metrics.memoryUsage / 1024 / 1024).toFixed(2)} MB`);
* console.log(`Total Requests: ${metrics.totalRequests}`);
* console.log(`Cache Size: ${metrics.keyCount} keys`);
* ```
*/
getMetrics(): CacheMetrics {
const stats = this.cache.getStats();
const totalRequests = this.metrics.hits + this.metrics.misses;
return {
hitRate: totalRequests > 0 ? this.metrics.hits / totalRequests : 0,
totalRequests,
hits: this.metrics.hits,
misses: this.metrics.misses,
evictions: this.metrics.evictions,
avgTtl: 0, // NodeCache doesn't provide this
memoryUsage: 0, // Would need process.memoryUsage() to estimate
keyCount: stats.keys,
};
}
/**
* Get all cache keys (for debugging)
*/
getKeys(): string[] {
return this.cache.keys();
}
/**
* Get cache statistics from NodeCache
*/
getStats() {
return this.cache.getStats();
}
/**
* Set TTL for existing key
*/
async setTTL(key: string, ttl: number): Promise<boolean> {
return this.cache.ttl(key, ttl);
}
/**
* Get TTL for key (remaining time in seconds)
*/
async getTTL(key: string): Promise<number> {
const expireTime = this.cache.getTtl(key);
if (expireTime === 0 || expireTime === undefined) {
return 0; // Key doesn't exist or has no TTL
}
const now = Date.now();
const remainingMs = expireTime - now;
return Math.max(0, Math.floor(remainingMs / 1000)); // Convert to seconds
}
/**
* Create a cache key from multiple parts
*/
static createKey(...parts: (string | number | boolean)[]): string {
return parts
.map(part => String(part))
.map(part => part.replace(/[^a-zA-Z0-9._@-]/g, '_')) // Allow @ symbol
.join(':');
}
/**
* Cleanup expired keys manually
*/
cleanup(): void {
// This is automatically handled by NodeCache, but can be called manually
// The cache will automatically remove expired keys during normal operations
}
}