UNPKG

ms365-mcp-server

Version:

Microsoft 365 MCP Server for managing Microsoft 365 email through natural language interactions with full OAuth2 authentication support

380 lines (379 loc) โ€ข 11.7 kB
import { logger } from './api.js'; export class IntelligentCache { constructor(maxSize = 50 * 1024 * 1024, // 50MB default maxEntries = 10000, defaultTTL = 5 * 60 * 1000 // 5 minutes default ) { this.cache = new Map(); this.stats = { hits: 0, misses: 0, entries: 0, size: 0, hitRate: 0, evictions: 0 }; this.maxSize = maxSize; this.maxEntries = maxEntries; this.defaultTTL = defaultTTL; // Cleanup expired entries every minute setInterval(() => this.cleanup(), 60000); } /** * Get value from cache */ get(key) { const entry = this.cache.get(key); if (!entry) { this.stats.misses++; this.updateHitRate(); return undefined; } // Check if entry is expired if (this.isExpired(entry)) { this.cache.delete(key); this.stats.misses++; this.stats.entries--; this.stats.size -= entry.size; this.updateHitRate(); return undefined; } // Update access statistics entry.accessCount++; entry.lastAccessed = Date.now(); this.stats.hits++; this.updateHitRate(); return entry.value; } /** * Set value in cache */ set(key, value, options = {}) { const now = Date.now(); const size = this.estimateSize(value); const ttl = options.ttl || this.defaultTTL; // Check if we need to evict entries if (this.shouldEvict(size)) { this.evictEntries(size); } // Create cache entry const entry = { key, value, timestamp: now, accessCount: 1, lastAccessed: now, ttl, size, tags: options.tags || [] }; // Remove existing entry if it exists if (this.cache.has(key)) { const existingEntry = this.cache.get(key); this.stats.size -= existingEntry.size; this.stats.entries--; } // Add new entry this.cache.set(key, entry); this.stats.size += size; this.stats.entries++; return true; } /** * Delete entry from cache */ delete(key) { const entry = this.cache.get(key); if (entry) { this.cache.delete(key); this.stats.size -= entry.size; this.stats.entries--; return true; } return false; } /** * Clear cache by tags */ clearByTags(tags) { let cleared = 0; for (const [key, entry] of this.cache) { if (entry.tags.some(tag => tags.includes(tag))) { this.cache.delete(key); this.stats.size -= entry.size; this.stats.entries--; cleared++; } } if (cleared > 0) { logger.log(`๐Ÿ—‘๏ธ Cleared ${cleared} cache entries by tags: ${tags.join(', ')}`); } return cleared; } /** * Clear all cache */ clear() { this.cache.clear(); this.stats.entries = 0; this.stats.size = 0; logger.log('๐Ÿ—‘๏ธ Cache cleared'); } /** * Get cache statistics */ getStats() { return { ...this.stats }; } /** * Get cache entries for debugging */ getEntries() { const now = Date.now(); return Array.from(this.cache.entries()).map(([key, entry]) => ({ key, size: entry.size, age: now - entry.timestamp, accessCount: entry.accessCount, tags: entry.tags })); } /** * Check if entry is expired */ isExpired(entry) { return Date.now() - entry.timestamp > entry.ttl; } /** * Estimate size of object */ estimateSize(obj) { try { return JSON.stringify(obj).length * 2; // Rough estimate (UTF-16) } catch { return 1024; // Default size if can't stringify } } /** * Check if we should evict entries */ shouldEvict(newEntrySize) { return (this.stats.size + newEntrySize > this.maxSize || this.stats.entries >= this.maxEntries); } /** * Evict entries using LRU strategy */ evictEntries(spaceNeeded) { const entries = Array.from(this.cache.entries()); // Sort by last accessed time (LRU) entries.sort((a, b) => a[1].lastAccessed - b[1].lastAccessed); let freedSpace = 0; let evicted = 0; for (const [key, entry] of entries) { if (freedSpace >= spaceNeeded && this.stats.entries < this.maxEntries) { break; } this.cache.delete(key); freedSpace += entry.size; this.stats.size -= entry.size; this.stats.entries--; evicted++; } this.stats.evictions += evicted; if (evicted > 0) { logger.log(`๐Ÿ—‘๏ธ Evicted ${evicted} cache entries (freed ${freedSpace} bytes)`); } } /** * Cleanup expired entries */ cleanup() { const now = Date.now(); let cleaned = 0; for (const [key, entry] of this.cache) { if (this.isExpired(entry)) { this.cache.delete(key); this.stats.size -= entry.size; this.stats.entries--; cleaned++; } } if (cleaned > 0) { logger.log(`๐Ÿงน Cleaned up ${cleaned} expired cache entries`); } } /** * Update hit rate */ updateHitRate() { const total = this.stats.hits + this.stats.misses; this.stats.hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0; } } export class MS365CacheManager { constructor() { // Different cache configurations for different data types this.searchCache = new IntelligentCache(20 * 1024 * 1024, // 20MB for search results 5000, // 5000 entries max 10 * 60 * 1000 // 10 minutes TTL ); this.emailCache = new IntelligentCache(30 * 1024 * 1024, // 30MB for email content 3000, // 3000 entries max 15 * 60 * 1000 // 15 minutes TTL ); this.metadataCache = new IntelligentCache(10 * 1024 * 1024, // 10MB for metadata 10000, // 10000 entries max 30 * 60 * 1000 // 30 minutes TTL ); this.attachmentCache = new IntelligentCache(100 * 1024 * 1024, // 100MB for attachments 1000, // 1000 entries max 60 * 60 * 1000 // 1 hour TTL ); } static getInstance() { if (!this.instance) { this.instance = new MS365CacheManager(); } return this.instance; } /** * Cache search results */ cacheSearchResults(query, results, folder = 'inbox') { const cacheKey = this.generateSearchKey(query, folder); this.searchCache.set(cacheKey, results, { ttl: 5 * 60 * 1000, // 5 minutes for search results tags: ['search', folder], priority: 'high' }); } /** * Get cached search results */ getCachedSearchResults(query, folder = 'inbox') { const cacheKey = this.generateSearchKey(query, folder); return this.searchCache.get(cacheKey); } /** * Cache email content */ cacheEmail(email) { this.emailCache.set(email.id, email, { ttl: 15 * 60 * 1000, // 15 minutes for email content tags: ['email', email.from.address], priority: 'medium' }); } /** * Get cached email */ getCachedEmail(emailId) { return this.emailCache.get(emailId); } /** * Cache email metadata */ cacheEmailMetadata(emailId, metadata) { this.metadataCache.set(`metadata:${emailId}`, metadata, { ttl: 30 * 60 * 1000, // 30 minutes for metadata tags: ['metadata'], priority: 'low' }); } /** * Get cached email metadata */ getCachedEmailMetadata(emailId) { return this.metadataCache.get(`metadata:${emailId}`); } /** * Cache attachment data */ cacheAttachment(messageId, attachmentId, attachmentData) { const cacheKey = `attachment:${messageId}:${attachmentId}`; this.attachmentCache.set(cacheKey, attachmentData, { ttl: 60 * 60 * 1000, // 1 hour for attachments tags: ['attachment', messageId], priority: 'medium' }); } /** * Get cached attachment */ getCachedAttachment(messageId, attachmentId) { const cacheKey = `attachment:${messageId}:${attachmentId}`; return this.attachmentCache.get(cacheKey); } /** * Invalidate caches when emails are modified */ invalidateEmailCaches(emailId) { this.emailCache.delete(emailId); this.metadataCache.delete(`metadata:${emailId}`); this.attachmentCache.clearByTags([emailId]); this.searchCache.clearByTags(['search']); // Clear search cache as it might be outdated } /** * Invalidate folder-specific caches */ invalidateFolderCaches(folder) { this.searchCache.clearByTags([folder]); } /** * Get comprehensive cache statistics */ getStats() { const searchStats = this.searchCache.getStats(); const emailStats = this.emailCache.getStats(); const metadataStats = this.metadataCache.getStats(); const attachmentStats = this.attachmentCache.getStats(); const totalSize = searchStats.size + emailStats.size + metadataStats.size + attachmentStats.size; const totalEntries = searchStats.entries + emailStats.entries + metadataStats.entries + attachmentStats.entries; const totalHits = searchStats.hits + emailStats.hits + metadataStats.hits + attachmentStats.hits; const totalMisses = searchStats.misses + emailStats.misses + metadataStats.misses + attachmentStats.misses; const totalHitRate = totalHits + totalMisses > 0 ? (totalHits / (totalHits + totalMisses)) * 100 : 0; return { search: searchStats, email: emailStats, metadata: metadataStats, attachment: attachmentStats, total: { size: totalSize, entries: totalEntries, hitRate: totalHitRate } }; } /** * Clear all caches */ clearAll() { this.searchCache.clear(); this.emailCache.clear(); this.metadataCache.clear(); this.attachmentCache.clear(); logger.log('๐Ÿ—‘๏ธ All caches cleared'); } /** * Generate cache key for search */ generateSearchKey(query, folder) { // Create a normalized cache key that's case-insensitive and handles variations const normalizedQuery = query.toLowerCase().trim().replace(/\s+/g, ' '); return `search:${folder}:${normalizedQuery}`; } /** * Optimize cache performance */ optimize() { // Force cleanup of expired entries this.searchCache['cleanup'](); this.emailCache['cleanup'](); this.metadataCache['cleanup'](); this.attachmentCache['cleanup'](); logger.log('๐Ÿ”ง Cache optimization completed'); } } // Export singleton instance export const ms365Cache = MS365CacheManager.getInstance();