Skip to main content
Glama
freefish1218

MCP HuggingFetch

by freefish1218
cache.js11 kB
/** * 智能缓存模块 - 提供内存缓存、LRU淘汰、TTL过期等功能 */ const crypto = require('crypto'); const { getLogger } = require('../utils/logger'); const logger = getLogger('Cache'); /** * 缓存条目类 */ class CacheEntry { constructor(data, ttl = 300000) { // 默认5分钟TTL this.data = data; this.expiry = Date.now() + ttl; this.lastAccessed = Date.now(); this.hitCount = 0; this.size = this.calculateSize(data); } /** * 估算数据大小(字节) */ calculateSize(data) { try { return Buffer.byteLength(JSON.stringify(data)); } catch { return 0; } } /** * 是否已过期 */ isExpired() { return Date.now() > this.expiry; } /** * 更新访问时间 */ touch() { this.lastAccessed = Date.now(); this.hitCount++; } } /** * 智能缓存类 */ class SmartCache { constructor(options = {}) { this.options = { maxSize: options.maxSize || 100, // 最大缓存条目数 maxMemory: options.maxMemory || 50 * 1024 * 1024, // 最大内存50MB defaultTTL: options.defaultTTL || 300000, // 默认5分钟 cleanupInterval: options.cleanupInterval || 60000, // 清理间隔1分钟 algorithm: options.algorithm || 'lru' // 淘汰算法:lru, lfu, fifo }; this.cache = new Map(); this.stats = { hits: 0, misses: 0, evictions: 0, memoryUsed: 0 }; // ETags缓存(用于条件请求) this.etags = new Map(); // 启动定期清理 this.startCleanup(); } /** * 生成缓存键 */ getCacheKey(params) { // 规范化参数 const normalized = this.normalizeParams(params); const keyStr = JSON.stringify(normalized); // 使用hash避免键过长 return crypto .createHash('md5') .update(keyStr) .digest('hex'); } /** * 规范化参数(确保相同参数生成相同的键) */ normalizeParams(params) { if (typeof params === 'string') { return params; } if (typeof params !== 'object' || params === null) { return params; } // 递归排序对象键 const sorted = {}; Object.keys(params) .sort() .forEach(key => { const value = params[key]; if (value !== undefined && value !== null) { sorted[key] = typeof value === 'object' && !Array.isArray(value) ? this.normalizeParams(value) : value; } }); return sorted; } /** * 获取缓存 */ get(key, params = null) { const cacheKey = params ? this.getCacheKey({ key, ...params }) : key; const entry = this.cache.get(cacheKey); if (!entry) { this.stats.misses++; logger.debug(`缓存未命中: ${cacheKey}`); return null; } // 检查过期 if (entry.isExpired()) { this.cache.delete(cacheKey); this.stats.memoryUsed -= entry.size; this.stats.misses++; logger.debug(`缓存已过期: ${cacheKey}`); return null; } // 更新访问信息 entry.touch(); this.stats.hits++; logger.debug(`缓存命中: ${cacheKey} (第${entry.hitCount}次)`); return entry.data; } /** * 设置缓存 */ set(key, data, ttl = null, params = null) { const cacheKey = params ? this.getCacheKey({ key, ...params }) : key; const effectiveTTL = ttl || this.options.defaultTTL; // 创建缓存条目 const entry = new CacheEntry(data, effectiveTTL); // 检查内存限制 if (this.stats.memoryUsed + entry.size > this.options.maxMemory) { this.evict(entry.size); } // 检查条目数限制 if (this.cache.size >= this.options.maxSize) { this.evictOne(); } // 更新已存在的条目 const oldEntry = this.cache.get(cacheKey); if (oldEntry) { this.stats.memoryUsed -= oldEntry.size; } // 添加新条目 this.cache.set(cacheKey, entry); this.stats.memoryUsed += entry.size; logger.debug(`缓存设置: ${cacheKey}, TTL: ${effectiveTTL}ms, 大小: ${entry.size}字节`); return true; } /** * 删除缓存 */ delete(key, params = null) { const cacheKey = params ? this.getCacheKey({ key, ...params }) : key; const entry = this.cache.get(cacheKey); if (entry) { this.cache.delete(cacheKey); this.stats.memoryUsed -= entry.size; logger.debug(`缓存删除: ${cacheKey}`); return true; } return false; } /** * 清空缓存 */ clear() { const size = this.cache.size; this.cache.clear(); this.etags.clear(); this.stats.memoryUsed = 0; logger.info(`清空缓存: 删除了 ${size} 个条目`); } /** * 淘汰缓存(根据配置的算法) */ evict(requiredSpace = 0) { const algorithm = this.options.algorithm; switch (algorithm) { case 'lru': this.evictLRU(requiredSpace); break; case 'lfu': this.evictLFU(requiredSpace); break; case 'fifo': this.evictFIFO(requiredSpace); break; default: this.evictLRU(requiredSpace); } } /** * LRU淘汰(最近最少使用) */ evictLRU(requiredSpace = 0) { const entries = Array.from(this.cache.entries()); entries.sort((a, b) => a[1].lastAccessed - b[1].lastAccessed); let freedSpace = 0; for (const [key, entry] of entries) { this.cache.delete(key); freedSpace += entry.size; this.stats.evictions++; this.stats.memoryUsed -= entry.size; logger.debug(`LRU淘汰: ${key}`); if (freedSpace >= requiredSpace) { break; } } } /** * LFU淘汰(最少使用频率) */ evictLFU(requiredSpace = 0) { const entries = Array.from(this.cache.entries()); entries.sort((a, b) => a[1].hitCount - b[1].hitCount); let freedSpace = 0; for (const [key, entry] of entries) { this.cache.delete(key); freedSpace += entry.size; this.stats.evictions++; this.stats.memoryUsed -= entry.size; logger.debug(`LFU淘汰: ${key} (命中${entry.hitCount}次)`); if (freedSpace >= requiredSpace) { break; } } } /** * FIFO淘汰(先进先出) */ evictFIFO(requiredSpace = 0) { let freedSpace = 0; for (const [key, entry] of this.cache) { this.cache.delete(key); freedSpace += entry.size; this.stats.evictions++; this.stats.memoryUsed -= entry.size; logger.debug(`FIFO淘汰: ${key}`); if (freedSpace >= requiredSpace) { break; } } } /** * 淘汰一个条目 */ evictOne() { if (this.cache.size === 0) return; const algorithm = this.options.algorithm; let keyToEvict = null; switch (algorithm) { case 'lru': { // 找到最久未访问的 let oldestTime = Infinity; for (const [key, entry] of this.cache) { if (entry.lastAccessed < oldestTime) { oldestTime = entry.lastAccessed; keyToEvict = key; } } break; } case 'lfu': { // 找到使用次数最少的 let minHits = Infinity; for (const [key, entry] of this.cache) { if (entry.hitCount < minHits) { minHits = entry.hitCount; keyToEvict = key; } } break; } default: // fifo // 删除第一个 keyToEvict = this.cache.keys().next().value; } if (keyToEvict) { const entry = this.cache.get(keyToEvict); this.cache.delete(keyToEvict); this.stats.memoryUsed -= entry.size; this.stats.evictions++; logger.debug(`淘汰缓存: ${keyToEvict}`); } } /** * 定期清理过期缓存 */ startCleanup() { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); } this.cleanupTimer = setInterval(() => { this.cleanup(); }, this.options.cleanupInterval); // Node.js进程退出时清理 if (typeof process !== 'undefined') { process.on('exit', () => { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); } }); } } /** * 停止定期清理 */ stopCleanup() { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); this.cleanupTimer = null; } } /** * 销毁缓存实例(清理所有资源) */ destroy() { this.stopCleanup(); this.cache.clear(); this.etags.clear(); this.stats = { hits: 0, misses: 0, evictions: 0, memoryUsed: 0 }; } /** * 清理过期条目 */ cleanup() { let cleaned = 0; const now = Date.now(); for (const [key, entry] of this.cache) { if (now > entry.expiry) { this.cache.delete(key); this.stats.memoryUsed -= entry.size; cleaned++; } } if (cleaned > 0) { logger.debug(`清理了 ${cleaned} 个过期缓存条目`); } } /** * 获取缓存统计 */ getStats() { const hitRate = this.stats.hits + this.stats.misses > 0 ? this.stats.hits / (this.stats.hits + this.stats.misses) : 0; return { ...this.stats, hitRate: Math.round(hitRate * 100) / 100, size: this.cache.size, memoryUsedMB: Math.round(this.stats.memoryUsed / 1024 / 1024 * 100) / 100 }; } /** * ETag支持 - 获取ETag */ getETag(url) { return this.etags.get(url); } /** * ETag支持 - 设置ETag */ setETag(url, etag) { this.etags.set(url, etag); } /** * 条件请求支持 */ async fetchWithETag(url, fetchFn) { const etag = this.getETag(url); try { const headers = {}; if (etag) { headers['If-None-Match'] = etag; } const response = await fetchFn(url, { headers }); // 304 Not Modified if (response.status === 304) { logger.debug(`ETag命中,使用缓存: ${url}`); return { cached: true, data: this.get(url) }; } // 更新ETag const newETag = response.headers?.etag; if (newETag) { this.setETag(url, newETag); } return { cached: false, data: response.data, headers: response.headers }; } catch (error) { // 发生错误时尝试返回缓存 const cached = this.get(url); if (cached) { logger.warn(`请求失败,返回缓存数据: ${url}`); return { cached: true, data: cached, fallback: true }; } throw error; } } /** * 停止缓存服务 */ stop() { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); this.cleanupTimer = null; } this.clear(); } } /** * 创建缓存实例 */ function createCache(options) { return new SmartCache(options); } /** * 默认缓存实例 */ let defaultCache = null; function getDefaultCache() { if (!defaultCache) { defaultCache = createCache(); } return defaultCache; } module.exports = { SmartCache, CacheEntry, createCache, getDefaultCache };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/freefish1218/mcp-huggingfetch'

If you have feedback or need assistance with the MCP directory API, please join our Discord server