Skip to main content
Glama
ProjectCache.ts5.79 kB
// Project caching utility for ts-morph (v1.4) // Implements LRU cache with memory limits to avoid re-parsing on every request import { Project } from 'ts-morph'; import path from 'path'; interface CachedProject { project: Project; lastAccess: number; fileCount: number; estimatedMemoryMB: number; hitCount: number; } interface CacheStats { size: number; totalMemoryMB: number; hitRate: number; projects: Array<{ path: string; files: number; memoryMB: number; age: number; hits: number; }>; } export class ProjectCache { private static instance: ProjectCache | null = null; private cache = new Map<string, CachedProject>(); private totalHits = 0; private totalMisses = 0; private lastCleanup = Date.now(); // Configuration constants private readonly MAX_CACHE_SIZE = 5; private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes private readonly MAX_TOTAL_MEMORY_MB = 200; private readonly MAX_PROJECT_MEMORY_MB = 100; private readonly MEMORY_PER_FILE_MB = 0.5; private readonly BASE_MEMORY_MB = 1; private readonly CLEANUP_INTERVAL = 60 * 1000; // 1 minute private constructor() {} public static getInstance(): ProjectCache { if (!ProjectCache.instance) { ProjectCache.instance = new ProjectCache(); } return ProjectCache.instance; } public getOrCreate(projectPath: string): Project { // Normalize path and remove trailing slashes let normalizedPath = path.normalize(projectPath); if (normalizedPath.endsWith(path.sep) && normalizedPath.length > 1) { normalizedPath = normalizedPath.slice(0, -1); } const now = Date.now(); // Check if cached and not expired const cached = this.cache.get(normalizedPath); if (cached && (now - cached.lastAccess) < this.CACHE_TTL) { cached.lastAccess = now; cached.hitCount++; this.totalHits++; return cached.project; } this.totalMisses++; // Lazy cleanup - only run periodically to reduce overhead if (now - this.lastCleanup > this.CLEANUP_INTERVAL) { this.removeExpired(); this.lastCleanup = now; } // LRU eviction if cache is full if (this.cache.size >= this.MAX_CACHE_SIZE) { this.evictLRU(); } // Create new project const project = new Project({ useInMemoryFileSystem: false, compilerOptions: { allowJs: true, skipLibCheck: true, noEmit: true } }); // Add source files const pattern = path.join(normalizedPath, '**/*.{ts,tsx,js,jsx}'); project.addSourceFilesAtPaths(pattern); const sourceFiles = project.getSourceFiles(); const fileCount = sourceFiles.length; // Estimate memory usage: base + per-file overhead const estimatedMemoryMB = this.BASE_MEMORY_MB + (fileCount * this.MEMORY_PER_FILE_MB); // Skip caching if project is too large if (estimatedMemoryMB > this.MAX_PROJECT_MEMORY_MB) { console.warn(`Project ${normalizedPath} too large (${estimatedMemoryMB.toFixed(1)}MB, ${fileCount} files) - skipping cache`); return project; } // Check total cache memory before adding const totalMemory = this.getTotalMemoryUsage(); if (totalMemory + estimatedMemoryMB > this.MAX_TOTAL_MEMORY_MB) { // Evict projects until we have enough space while (this.getTotalMemoryUsage() + estimatedMemoryMB > this.MAX_TOTAL_MEMORY_MB && this.cache.size > 0) { this.evictLRU(); } } this.cache.set(normalizedPath, { project, lastAccess: now, fileCount, estimatedMemoryMB, hitCount: 0 }); return project; } public invalidate(projectPath: string): void { const normalizedPath = path.normalize(projectPath); this.cache.delete(normalizedPath); } public clear(): void { this.cache.clear(); } public getStats(): CacheStats { const now = Date.now(); const projects = Array.from(this.cache.entries()).map(([projectPath, cached]) => ({ path: projectPath, files: cached.fileCount, memoryMB: cached.estimatedMemoryMB, age: Math.floor((now - cached.lastAccess) / 1000), hits: cached.hitCount })); const totalRequests = this.totalHits + this.totalMisses; const hitRate = totalRequests > 0 ? this.totalHits / totalRequests : 0; return { size: this.cache.size, totalMemoryMB: this.getTotalMemoryUsage(), hitRate: Math.round(hitRate * 100) / 100, projects }; } private getTotalMemoryUsage(): number { let total = 0; this.cache.forEach(cached => { total += cached.estimatedMemoryMB; }); return total; } private removeExpired(): void { const now = Date.now(); const toRemove: string[] = []; this.cache.forEach((cached, path) => { if ((now - cached.lastAccess) >= this.CACHE_TTL) { toRemove.push(path); } }); toRemove.forEach(path => this.cache.delete(path)); } private evictLRU(): void { let victimPath: string | null = null; let lowestScore = Infinity; // LRU-K variant: consider both recency and frequency const now = Date.now(); this.cache.forEach((cached, projectPath) => { // Score = recency weight + frequency weight // Lower score = better candidate for eviction const age = now - cached.lastAccess; const recencyScore = age / this.CACHE_TTL; // 0-1, higher = older const frequencyScore = 1 / (cached.hitCount + 1); // Higher hits = lower score const score = recencyScore * 0.7 + frequencyScore * 0.3; if (score > lowestScore || victimPath === null) { lowestScore = score; victimPath = projectPath; } }); if (victimPath) { this.cache.delete(victimPath); } } }

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/ssdeanx/ssd-ai'

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