openapi-directory-mcp
Version:
Model Context Protocol server for accessing enhanced triple-source OpenAPI directory (APIs.guru + additional APIs + custom imports)
391 lines • 11.5 kB
JavaScript
import NodeCache from "node-cache";
import { CACHE_TTL, CACHE_LIMITS } from "../utils/constants.js";
import { CacheValidator } from "../utils/validation.js";
import { CacheError, ErrorHandler } from "../utils/errors.js";
import { Logger } from "../utils/logger.js";
export class CacheManager {
constructor(ttlMs = CACHE_TTL.DEFAULT) {
this.enabled = process.env.DISABLE_CACHE !== "true";
this.cache = new NodeCache({
stdTTL: Math.floor(ttlMs / 1000), // NodeCache uses seconds
checkperiod: process.env.NODE_ENV === "test"
? 0
: Math.floor((ttlMs / 1000) * CACHE_LIMITS.CHECK_PERIOD_RATIO), // Disable periodic checks in tests
useClones: false, // Better performance, but be careful with mutations
deleteOnExpire: true,
maxKeys: CACHE_LIMITS.MAX_KEYS,
});
// Set up cache statistics logging
this.cache.on("expired", (key) => {
Logger.cache("expired", key);
});
this.cache.on("set", (key) => {
Logger.cache("set", key);
});
}
/**
* Get value from cache with validation
*/
get(key) {
if (!this.enabled) {
return undefined;
}
try {
const wrappedValue = this.cache.get(key);
if (wrappedValue !== undefined) {
// Validate cache entry integrity
if (!this.validateCacheEntry(key, wrappedValue)) {
Logger.warn(`Cache corruption detected for key: ${key}`);
this.delete(key);
return undefined;
}
Logger.cache("hit", key);
// Return the unwrapped value
return wrappedValue.value;
}
return undefined;
}
catch (error) {
const cacheError = new CacheError(`Cache get error for key ${key}`, {
operation: "get",
details: {
key,
error: error instanceof Error ? error.message : String(error),
},
});
ErrorHandler.logError(cacheError);
return undefined;
}
}
/**
* Set value in cache with enhanced metadata
*/
set(key, value, ttlMs) {
if (!this.enabled) {
return false;
}
try {
// Wrap value with metadata for validation
const wrappedValue = {
value,
timestamp: new Date().toISOString(),
integrity: CacheValidator.generateIntegrityHash(value),
};
const success = ttlMs
? this.cache.set(key, wrappedValue, Math.max(1, Math.floor(ttlMs / 1000)))
: this.cache.set(key, wrappedValue);
if (success) {
const ttlSeconds = ttlMs
? Math.max(1, Math.floor(ttlMs / 1000))
: "default";
Logger.cache("set", key, { ttl: `${ttlSeconds}s` });
}
return success;
}
catch (error) {
const cacheError = new CacheError(`Cache set error for key ${key}`, {
operation: "set",
details: {
key,
error: error instanceof Error ? error.message : String(error),
},
});
ErrorHandler.logError(cacheError);
return false;
}
}
/**
* Delete value from cache
*/
delete(key) {
if (!this.enabled) {
return 0;
}
try {
const deleted = this.cache.del(key);
if (deleted > 0) {
Logger.cache("delete", key);
}
return deleted;
}
catch (error) {
Logger.error(`Cache delete error for key ${key}:`, error);
return 0;
}
}
/**
* Clear all cache entries
*/
clear() {
if (!this.enabled) {
return;
}
try {
this.cache.flushAll();
Logger.cache("clear", "all");
}
catch (error) {
Logger.error("Cache clear error:", error);
}
}
/**
* Destroy the cache and clean up resources
*/
destroy() {
if (!this.enabled)
return;
try {
// Clear all data
this.cache.flushAll();
// Close the cache to stop any internal timers
if (typeof this.cache.close === "function") {
this.cache.close();
}
}
catch (error) {
Logger.error("Cache destroy error:", error);
}
}
/**
* Get cache statistics
*/
getStats() {
if (!this.enabled) {
return {
keys: 0,
hits: 0,
misses: 0,
ksize: 0,
vsize: 0,
};
}
return this.cache.getStats();
}
/**
* Get all cache keys
*/
keys() {
if (!this.enabled) {
return [];
}
return this.cache.keys();
}
/**
* Check if cache has a key
*/
has(key) {
if (!this.enabled) {
return false;
}
return this.cache.has(key);
}
/**
* Get TTL for a key (in seconds)
*/
getTtl(key) {
if (!this.enabled) {
return undefined;
}
const ttl = this.cache.getTtl(key);
// NodeCache returns 0 for non-existent keys
return ttl === 0 ? undefined : ttl;
}
/**
* Get cache size information
*/
getSize() {
if (!this.enabled) {
return 0;
}
return this.cache.keys().length;
}
/**
* Get cache memory usage (approximate)
*/
getMemoryUsage() {
if (!this.enabled) {
return 0;
}
const stats = this.cache.getStats();
return stats.vsize + stats.ksize;
}
/**
* Prune expired entries manually
*/
prune() {
if (!this.enabled) {
return;
}
// NodeCache handles this automatically, but we can force it
const keys = this.cache.keys();
const now = Date.now();
for (const key of keys) {
const ttl = this.cache.getTtl(key);
if (ttl && ttl < now) {
this.cache.del(key);
}
}
}
/**
* Set cache enabled/disabled
*/
setEnabled(enabled) {
this.enabled = enabled;
if (!enabled) {
this.clear();
}
}
/**
* Check if cache is enabled
*/
isEnabled() {
return this.enabled;
}
/**
* Get cache configuration
*/
getConfig() {
return {
enabled: this.enabled,
ttlSeconds: this.cache.options.stdTTL || 86400,
maxKeys: this.cache.options.maxKeys || 1000,
};
}
/**
* Invalidate cache keys matching a pattern (supports * wildcard)
*/
invalidatePattern(pattern) {
if (!this.enabled) {
return 0;
}
try {
const keys = this.keys();
const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
let deletedCount = 0;
for (const key of keys) {
if (regex.test(key)) {
deletedCount += this.cache.del(key);
}
}
if (deletedCount > 0) {
Logger.cache("invalidatePattern", pattern, { count: deletedCount });
}
return deletedCount;
}
catch (error) {
Logger.error(`Cache invalidatePattern error for pattern ${pattern}:`, error);
return 0;
}
}
/**
* Invalidate multiple specific cache keys
*/
invalidateKeys(keys) {
if (!this.enabled) {
return 0;
}
try {
let deletedCount = 0;
for (const key of keys) {
deletedCount += this.cache.del(key);
}
if (deletedCount > 0) {
Logger.cache("invalidateKeys", "multiple", { count: deletedCount });
}
return deletedCount;
}
catch (error) {
Logger.error(`Cache invalidateKeys error:`, error);
return 0;
}
}
/**
* Cache warming - fetch data and store it in cache
*/
async warmCache(key, fetchFn, ttlMs) {
if (!this.enabled) {
// If cache is disabled, just fetch and return the data
return await fetchFn();
}
try {
// Check if we already have fresh data
const cached = this.get(key);
if (cached !== undefined) {
return cached;
}
// Fetch fresh data
Logger.cache("warming", key);
const data = await fetchFn();
// Store in cache
this.set(key, data, ttlMs);
return data;
}
catch (error) {
Logger.error(`Cache warmCache error for key ${key}:`, error);
// On error, try to fetch without caching
return await fetchFn();
}
}
/**
* Validate cache entry integrity
*/
validateCacheEntry(key, wrappedValue) {
try {
// Check if it's a wrapped value with metadata
if (!wrappedValue || typeof wrappedValue !== "object") {
return false;
}
// For backward compatibility, allow unwrapped values
if (!wrappedValue.value &&
!wrappedValue.timestamp &&
!wrappedValue.integrity) {
return true; // Old format, assume valid
}
// Use cache validator for integrity check
if (!CacheValidator.validateCacheEntry(key, wrappedValue)) {
return false;
}
// Verify integrity hash if present
if (wrappedValue.integrity) {
return CacheValidator.verifyCacheIntegrity(wrappedValue.value, wrappedValue.integrity);
}
return true;
}
catch (error) {
Logger.error(`Cache validation error for key ${key}:`, error);
return false;
}
}
/**
* Perform cache health check and cleanup
*/
performHealthCheck() {
if (!this.enabled) {
return { totalKeys: 0, corruptedKeys: 0, cleanedKeys: 0, memoryUsage: 0 };
}
const keys = this.keys();
let corruptedKeys = 0;
let cleanedKeys = 0;
for (const key of keys) {
const wrappedValue = this.cache.get(key);
if (!this.validateCacheEntry(key, wrappedValue)) {
this.delete(key);
corruptedKeys++;
cleanedKeys++;
}
}
const memoryUsage = this.getMemoryUsage();
Logger.cache("healthCheck", "completed", {
cleaned: cleanedKeys,
total: keys.length,
corrupted: corruptedKeys,
});
return {
totalKeys: keys.length,
corruptedKeys,
cleanedKeys,
memoryUsage,
};
}
}
//# sourceMappingURL=manager.js.map