Skip to main content
Glama

SQL MCP Server

by polarisxb
MIT License
1
5
  • Apple
  • Linux
memory.ts9.93 kB
import { ICache, CacheOptions, CacheStats, CacheEntry } from './interface.js'; /** * 内存缓存实现 * 基于Map的高性能内存缓存,支持TTL过期和LRU淘汰 */ export class MemoryCache implements ICache { /** * 存储缓存条目的Map * 键为命名空间前缀的缓存键,值为缓存条目 */ private cache: Map<string, CacheEntry<any>> = new Map(); /** * 缓存统计 */ private stats = { hits: 0, misses: 0, lastCleanup: new Date() }; /** * 缓存配置选项 */ private readonly options: Required<CacheOptions>; /** * 当前命名空间 */ private readonly namespace: string; /** * 定时清理任务的计时器ID */ private cleanupTimer: NodeJS.Timeout | null = null; /** * 构造内存缓存实例 * @param options 缓存选项 */ constructor(options: CacheOptions = {}) { // 初始化默认配置 this.options = { ttl: options.ttl ?? 3600, // 默认1小时 maxSize: options.maxSize ?? 1000, namespace: options.namespace ?? 'default', cleanupInterval: options.cleanupInterval ?? 300000, // 默认5分钟 compression: options.compression ?? false, // 内存缓存不需要压缩 serialization: options.serialization ?? true }; this.namespace = this.options.namespace; // 设置定期清理 if (this.options.cleanupInterval > 0) { this.cleanupTimer = setInterval(() => { this.cleanup().catch(err => console.error('Cache cleanup error:', err)); }, this.options.cleanupInterval); // 确保定时器不会阻止Node进程退出 if (this.cleanupTimer.unref) { this.cleanupTimer.unref(); } } } /** * 获取带有命名空间前缀的键 * @param key 原始键 * @returns 带前缀的键 */ private getNamespacedKey(key: string): string { return `${this.namespace}:${key}`; } /** * 清理过期的缓存条目 */ async cleanup(): Promise<void> { const now = Date.now(); let expiredCount = 0; for (const [key, entry] of this.cache.entries()) { if (entry.expiry !== null && entry.expiry < now) { this.cache.delete(key); expiredCount++; } } this.stats.lastCleanup = new Date(); // 当缓存大小超过限制时,强制执行LRU淘汰 if (this.cache.size > this.options.maxSize) { await this.ensureCapacity(); } } /** * 序列化值 */ private serialize<T>(value: T): T { if (!this.options.serialization) { return value; } try { // 序列化后再解析,创建深拷贝并验证可序列化性 return JSON.parse(JSON.stringify(value)); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`缓存值无法序列化: ${errorMessage}`); } } /** * 从缓存中获取值 * @param key 缓存键 * @returns 缓存的值或undefined(不存在或已过期) */ async get<T>(key: string): Promise<T | undefined> { const namespacedKey = this.getNamespacedKey(key); const entry = this.cache.get(namespacedKey); if (!entry) { this.stats.misses++; return undefined; } const now = Date.now(); // 检查是否已过期 if (entry.expiry !== null && entry.expiry < now) { this.cache.delete(namespacedKey); this.stats.misses++; return undefined; } // 更新最后访问时间和命中次数 entry.lastAccessed = now; entry.hitCount = (entry.hitCount || 0) + 1; this.stats.hits++; return entry.value as T; } /** * 设置缓存值 * @param key 缓存键 * @param value 要缓存的值 * @param ttl 可选的TTL覆盖(秒) */ async set<T>(key: string, value: T, ttl?: number): Promise<void> { const namespacedKey = this.getNamespacedKey(key); const now = Date.now(); // 计算过期时间 let expiry: number | null = null; if (ttl !== undefined) { expiry = ttl > 0 ? now + ttl * 1000 : null; } else if (this.options.ttl > 0) { expiry = now + this.options.ttl * 1000; } // 尝试序列化,确保可存储 const serializedValue = this.serialize(value); // 当达到大小限制时,强制执行LRU淘汰 // 注意:必须在添加新项之前执行,并且需要排除当前键可能已经存在的情况 if (!this.cache.has(namespacedKey) && this.cache.size >= this.options.maxSize) { this.evictLRU(); } // 创建或更新缓存条目 this.cache.set(namespacedKey, { value: serializedValue, expiry, lastAccessed: now, createdAt: now, hitCount: 0 }); } /** * LRU淘汰:删除最近最少使用的缓存项 * 被设置为独立方法以便于测试和维护 */ private evictLRU(): void { // 如果没有达到最大大小,不需要淘汰 if (this.cache.size < this.options.maxSize) { return; } // 按最后访问时间排序所有条目 const entries = Array.from(this.cache.entries()) .sort((a, b) => a[1].lastAccessed - b[1].lastAccessed); // 只删除一个最旧的条目 if (entries.length > 0) { const oldestKey = entries[0][0]; this.cache.delete(oldestKey); } } /** * 确保缓存不超过最大容量 * 使用LRU策略淘汰最近最少使用的项 */ private async ensureCapacity(): Promise<void> { this.evictLRU(); } /** * 检查键是否存在且未过期 * @param key 缓存键 * @returns 布尔值表示键是否有效 */ async has(key: string): Promise<boolean> { const namespacedKey = this.getNamespacedKey(key); const entry = this.cache.get(namespacedKey); if (!entry) { return false; } // 检查是否已过期 if (entry.expiry !== null && entry.expiry < Date.now()) { this.cache.delete(namespacedKey); return false; } // has方法不应该更新访问时间,否则会影响LRU算法 // 这就是之前测试失败的原因 - has方法意外更新了访问时间 return true; } /** * 删除缓存键 * @param key 要删除的缓存键 * @returns 布尔值表示是否成功删除 */ async delete(key: string): Promise<boolean> { const namespacedKey = this.getNamespacedKey(key); return this.cache.delete(namespacedKey); } /** * 清除当前命名空间的所有缓存 */ async clear(): Promise<void> { // 只清除当前命名空间的缓存 const prefix = `${this.namespace}:`; for (const key of this.cache.keys()) { if (key.startsWith(prefix)) { this.cache.delete(key); } } } /** * 批量获取多个缓存键的值 * @param keys 缓存键数组 * @returns 值数组,未找到的项为undefined */ async getMany<T>(keys: string[]): Promise<(T | undefined)[]> { return Promise.all(keys.map(key => this.get<T>(key))); } /** * 批量设置多个缓存键值对 * @param entries 键值对数组 * @param ttl 可选的TTL覆盖(秒) */ async setMany<T>(entries: [string, T][], ttl?: number): Promise<void> { await Promise.all(entries.map(([key, value]) => this.set(key, value, ttl))); } /** * 批量删除多个缓存键 * @param keys 要删除的缓存键数组 * @returns 成功删除的数量 */ async deleteMany(keys: string[]): Promise<number> { const results = await Promise.all(keys.map(key => this.delete(key))); return results.filter(Boolean).length; } /** * 获取缓存统计信息 * @returns 缓存统计对象 */ async getStats(): Promise<CacheStats> { const prefix = `${this.namespace}:`; const keys = Array.from(this.cache.keys()) .filter(key => key.startsWith(prefix)) .map(key => key.substring(prefix.length)); // 计算近似内存使用量 let memoryUsage = 0; try { for (const key of keys) { const entry = await this.get(key); if (entry) { memoryUsage += JSON.stringify(entry).length * 2; // 每个字符约2字节 } } } catch (error) { // 忽略内存计算错误 } return { hits: this.stats.hits, misses: this.stats.misses, size: keys.length, keys, namespace: this.namespace, memoryUsage, lastCleanup: this.stats.lastCleanup }; } /** * 获取当前命名空间的所有缓存键 * @returns 缓存键数组 */ async getKeys(): Promise<string[]> { const prefix = `${this.namespace}:`; return Array.from(this.cache.keys()) .filter(key => key.startsWith(prefix)) .map(key => key.substring(prefix.length)); } /** * 创建一个使用指定命名空间的新缓存实例 * @param namespace 命名空间 * @returns 新的缓存实例 */ withNamespace(namespace: string): ICache { return new MemoryCache({ ...this.options, namespace }); } /** * 获取缓存值,如果不存在则使用工厂函数设置并返回 * @param key 缓存键 * @param factory 缓存未命中时调用的工厂函数 * @param ttl 可选的TTL覆盖(秒) * @returns 缓存值或工厂函数结果 */ async getOrSet<T>(key: string, factory: () => Promise<T>, ttl?: number): Promise<T> { const cachedValue = await this.get<T>(key); if (cachedValue !== undefined) { return cachedValue; } const value = await factory(); await this.set(key, value, ttl); return value; } /** * 释放资源,停止定时清理 */ dispose(): void { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); this.cleanupTimer = null; } } }

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/polarisxb/sql-mcp'

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