Skip to main content
Glama
cache-manager.ts6.36 kB
// // File: cache-manager.ts // Brief: Manages caching of CodeWiki documentation with TTL and persistence // // Copyright (c) 2025 Chris Bunting <cbuntingde@gmail.com> // // This source code is licensed under the MIT license found in the // LICENSE file in the root directory of this source tree. // import fs from 'fs/promises'; import path from 'path'; export interface CachedDocumentation { owner: string; repo: string; content: string; lastUpdated: Date; metadata: { size: number; sections: string[]; lastCommit?: string | undefined; }; } export interface CacheEntry { docs: CachedDocumentation; timestamp: number; ttl: number; // Time to live in milliseconds } export class CacheManager { private cacheDir: string; private memoryCache: Map<string, CacheEntry> = new Map(); private defaultTtl: number = 24 * 60 * 60 * 1000; // 24 hours constructor(cacheDir?: string) { this.cacheDir = cacheDir || path.join(process.cwd(), '.codewiki-cache'); this.initializeCacheDir(); } private async initializeCacheDir(): Promise<void> { try { await fs.access(this.cacheDir); } catch { await fs.mkdir(this.cacheDir, { recursive: true }); } } private getCacheKey(owner: string, repo: string): string { return `${owner}/${repo}`; } private getCacheFilePath(owner: string, repo: string): string { const key = this.getCacheKey(owner, repo); return path.join(this.cacheDir, `${key.replace('/', '_')}.json`); } async get(owner: string, repo: string): Promise<CachedDocumentation | null> { const key = this.getCacheKey(owner, repo); // Check memory cache first const memoryEntry = this.memoryCache.get(key); if (memoryEntry && Date.now() - memoryEntry.timestamp < memoryEntry.ttl) { return memoryEntry.docs; } // Check disk cache try { const filePath = this.getCacheFilePath(owner, repo); const data = await fs.readFile(filePath, 'utf-8'); const entry: CacheEntry = JSON.parse(data); if (Date.now() - entry.timestamp < entry.ttl) { // Add to memory cache this.memoryCache.set(key, entry); return entry.docs; } else { // Expired, remove from disk await fs.unlink(filePath).catch(() => {}); } } catch { // File doesn't exist or can't be read } return null; } async set(owner: string, repo: string, docs: CachedDocumentation, ttl?: number): Promise<void> { const key = this.getCacheKey(owner, repo); const ttlMs = ttl || this.defaultTtl; const entry: CacheEntry = { docs, timestamp: Date.now(), ttl: ttlMs, }; // Update memory cache this.memoryCache.set(key, entry); // Update disk cache try { const filePath = this.getCacheFilePath(owner, repo); await fs.writeFile(filePath, JSON.stringify(entry, null, 2), 'utf-8'); } catch (error) { console.warn(`Failed to write cache file for ${key}:`, error); } } async clearRepository(owner: string, repo: string): Promise<void> { const key = this.getCacheKey(owner, repo); // Remove from memory cache this.memoryCache.delete(key); // Remove from disk cache try { const filePath = this.getCacheFilePath(owner, repo); await fs.unlink(filePath); } catch { // File doesn't exist or can't be deleted } } async clearAll(): Promise<void> { // Clear memory cache this.memoryCache.clear(); // Clear disk cache try { const files = await fs.readdir(this.cacheDir); await Promise.all( files.map(file => fs.unlink(path.join(this.cacheDir, file)).catch(() => {}) ) ); } catch { // Cache directory doesn't exist or can't be read } } async listRepositories(): Promise<Array<{ owner: string; repo: string; lastUpdated: Date; size: number }>> { const repositories: Array<{ owner: string; repo: string; lastUpdated: Date; size: number }> = []; // Check memory cache for (const [key, entry] of this.memoryCache.entries()) { if (Date.now() - entry.timestamp < entry.ttl) { const [owner, repo] = key.split('/'); if (owner && repo) { repositories.push({ owner, repo, lastUpdated: entry.docs.lastUpdated, size: entry.docs.metadata.size, }); } } } // Check disk cache try { const files = await fs.readdir(this.cacheDir); for (const file of files) { if (file.endsWith('.json')) { try { const filePath = path.join(this.cacheDir, file); const data = await fs.readFile(filePath, 'utf-8'); const entry: CacheEntry = JSON.parse(data); if (Date.now() - entry.timestamp < entry.ttl) { const [owner, repo] = file.replace('.json', '').split('_'); if (owner && repo) { repositories.push({ owner, repo, lastUpdated: entry.docs.lastUpdated, size: entry.docs.metadata.size, }); } } else { // Expired, remove it await fs.unlink(filePath).catch(() => {}); } } catch { // Skip corrupted cache files } } } } catch { // Cache directory doesn't exist or can't be read } return repositories.sort((a, b) => b.lastUpdated.getTime() - a.lastUpdated.getTime()); } async getCacheStats(): Promise<{ totalSize: number; entryCount: number; memoryEntries: number; diskEntries: number }> { const repos = await this.listRepositories(); const totalSize = repos.reduce((sum, repo) => sum + repo.size, 0); let memoryEntries = 0; let diskEntries = 0; for (const [, entry] of this.memoryCache.entries()) { if (Date.now() - entry.timestamp < entry.ttl) { memoryEntries++; } } try { const files = await fs.readdir(this.cacheDir); diskEntries = files.filter(file => file.endsWith('.json')).length; } catch { // Cache directory doesn't exist } return { totalSize, entryCount: repos.length, memoryEntries, diskEntries, }; } }

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/cbuntingde/codewiki-mcp-server'

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