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
JavaScript
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();