ms365-mcp-server 1.1.15 → 1.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,379 @@
1
+ import { logger } from './api.js';
2
+ export class IntelligentCache {
3
+ constructor(maxSize = 50 * 1024 * 1024, // 50MB default
4
+ maxEntries = 10000, defaultTTL = 5 * 60 * 1000 // 5 minutes default
5
+ ) {
6
+ this.cache = new Map();
7
+ this.stats = {
8
+ hits: 0,
9
+ misses: 0,
10
+ entries: 0,
11
+ size: 0,
12
+ hitRate: 0,
13
+ evictions: 0
14
+ };
15
+ this.maxSize = maxSize;
16
+ this.maxEntries = maxEntries;
17
+ this.defaultTTL = defaultTTL;
18
+ // Cleanup expired entries every minute
19
+ setInterval(() => this.cleanup(), 60000);
20
+ }
21
+ /**
22
+ * Get value from cache
23
+ */
24
+ get(key) {
25
+ const entry = this.cache.get(key);
26
+ if (!entry) {
27
+ this.stats.misses++;
28
+ this.updateHitRate();
29
+ return undefined;
30
+ }
31
+ // Check if entry is expired
32
+ if (this.isExpired(entry)) {
33
+ this.cache.delete(key);
34
+ this.stats.misses++;
35
+ this.stats.entries--;
36
+ this.stats.size -= entry.size;
37
+ this.updateHitRate();
38
+ return undefined;
39
+ }
40
+ // Update access statistics
41
+ entry.accessCount++;
42
+ entry.lastAccessed = Date.now();
43
+ this.stats.hits++;
44
+ this.updateHitRate();
45
+ return entry.value;
46
+ }
47
+ /**
48
+ * Set value in cache
49
+ */
50
+ set(key, value, options = {}) {
51
+ const now = Date.now();
52
+ const size = this.estimateSize(value);
53
+ const ttl = options.ttl || this.defaultTTL;
54
+ // Check if we need to evict entries
55
+ if (this.shouldEvict(size)) {
56
+ this.evictEntries(size);
57
+ }
58
+ // Create cache entry
59
+ const entry = {
60
+ key,
61
+ value,
62
+ timestamp: now,
63
+ accessCount: 1,
64
+ lastAccessed: now,
65
+ ttl,
66
+ size,
67
+ tags: options.tags || []
68
+ };
69
+ // Remove existing entry if it exists
70
+ if (this.cache.has(key)) {
71
+ const existingEntry = this.cache.get(key);
72
+ this.stats.size -= existingEntry.size;
73
+ this.stats.entries--;
74
+ }
75
+ // Add new entry
76
+ this.cache.set(key, entry);
77
+ this.stats.size += size;
78
+ this.stats.entries++;
79
+ return true;
80
+ }
81
+ /**
82
+ * Delete entry from cache
83
+ */
84
+ delete(key) {
85
+ const entry = this.cache.get(key);
86
+ if (entry) {
87
+ this.cache.delete(key);
88
+ this.stats.size -= entry.size;
89
+ this.stats.entries--;
90
+ return true;
91
+ }
92
+ return false;
93
+ }
94
+ /**
95
+ * Clear cache by tags
96
+ */
97
+ clearByTags(tags) {
98
+ let cleared = 0;
99
+ for (const [key, entry] of this.cache) {
100
+ if (entry.tags.some(tag => tags.includes(tag))) {
101
+ this.cache.delete(key);
102
+ this.stats.size -= entry.size;
103
+ this.stats.entries--;
104
+ cleared++;
105
+ }
106
+ }
107
+ if (cleared > 0) {
108
+ logger.log(`🗑️ Cleared ${cleared} cache entries by tags: ${tags.join(', ')}`);
109
+ }
110
+ return cleared;
111
+ }
112
+ /**
113
+ * Clear all cache
114
+ */
115
+ clear() {
116
+ this.cache.clear();
117
+ this.stats.entries = 0;
118
+ this.stats.size = 0;
119
+ logger.log('🗑️ Cache cleared');
120
+ }
121
+ /**
122
+ * Get cache statistics
123
+ */
124
+ getStats() {
125
+ return { ...this.stats };
126
+ }
127
+ /**
128
+ * Get cache entries for debugging
129
+ */
130
+ getEntries() {
131
+ const now = Date.now();
132
+ return Array.from(this.cache.entries()).map(([key, entry]) => ({
133
+ key,
134
+ size: entry.size,
135
+ age: now - entry.timestamp,
136
+ accessCount: entry.accessCount,
137
+ tags: entry.tags
138
+ }));
139
+ }
140
+ /**
141
+ * Check if entry is expired
142
+ */
143
+ isExpired(entry) {
144
+ return Date.now() - entry.timestamp > entry.ttl;
145
+ }
146
+ /**
147
+ * Estimate size of object
148
+ */
149
+ estimateSize(obj) {
150
+ try {
151
+ return JSON.stringify(obj).length * 2; // Rough estimate (UTF-16)
152
+ }
153
+ catch {
154
+ return 1024; // Default size if can't stringify
155
+ }
156
+ }
157
+ /**
158
+ * Check if we should evict entries
159
+ */
160
+ shouldEvict(newEntrySize) {
161
+ return (this.stats.size + newEntrySize > this.maxSize ||
162
+ this.stats.entries >= this.maxEntries);
163
+ }
164
+ /**
165
+ * Evict entries using LRU strategy
166
+ */
167
+ evictEntries(spaceNeeded) {
168
+ const entries = Array.from(this.cache.entries());
169
+ // Sort by last accessed time (LRU)
170
+ entries.sort((a, b) => a[1].lastAccessed - b[1].lastAccessed);
171
+ let freedSpace = 0;
172
+ let evicted = 0;
173
+ for (const [key, entry] of entries) {
174
+ if (freedSpace >= spaceNeeded && this.stats.entries < this.maxEntries) {
175
+ break;
176
+ }
177
+ this.cache.delete(key);
178
+ freedSpace += entry.size;
179
+ this.stats.size -= entry.size;
180
+ this.stats.entries--;
181
+ evicted++;
182
+ }
183
+ this.stats.evictions += evicted;
184
+ if (evicted > 0) {
185
+ logger.log(`🗑️ Evicted ${evicted} cache entries (freed ${freedSpace} bytes)`);
186
+ }
187
+ }
188
+ /**
189
+ * Cleanup expired entries
190
+ */
191
+ cleanup() {
192
+ const now = Date.now();
193
+ let cleaned = 0;
194
+ for (const [key, entry] of this.cache) {
195
+ if (this.isExpired(entry)) {
196
+ this.cache.delete(key);
197
+ this.stats.size -= entry.size;
198
+ this.stats.entries--;
199
+ cleaned++;
200
+ }
201
+ }
202
+ if (cleaned > 0) {
203
+ logger.log(`🧹 Cleaned up ${cleaned} expired cache entries`);
204
+ }
205
+ }
206
+ /**
207
+ * Update hit rate
208
+ */
209
+ updateHitRate() {
210
+ const total = this.stats.hits + this.stats.misses;
211
+ this.stats.hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0;
212
+ }
213
+ }
214
+ export class MS365CacheManager {
215
+ constructor() {
216
+ // Different cache configurations for different data types
217
+ this.searchCache = new IntelligentCache(20 * 1024 * 1024, // 20MB for search results
218
+ 5000, // 5000 entries max
219
+ 10 * 60 * 1000 // 10 minutes TTL
220
+ );
221
+ this.emailCache = new IntelligentCache(30 * 1024 * 1024, // 30MB for email content
222
+ 3000, // 3000 entries max
223
+ 15 * 60 * 1000 // 15 minutes TTL
224
+ );
225
+ this.metadataCache = new IntelligentCache(10 * 1024 * 1024, // 10MB for metadata
226
+ 10000, // 10000 entries max
227
+ 30 * 60 * 1000 // 30 minutes TTL
228
+ );
229
+ this.attachmentCache = new IntelligentCache(100 * 1024 * 1024, // 100MB for attachments
230
+ 1000, // 1000 entries max
231
+ 60 * 60 * 1000 // 1 hour TTL
232
+ );
233
+ }
234
+ static getInstance() {
235
+ if (!this.instance) {
236
+ this.instance = new MS365CacheManager();
237
+ }
238
+ return this.instance;
239
+ }
240
+ /**
241
+ * Cache search results
242
+ */
243
+ cacheSearchResults(query, results, folder = 'inbox') {
244
+ const cacheKey = this.generateSearchKey(query, folder);
245
+ this.searchCache.set(cacheKey, results, {
246
+ ttl: 5 * 60 * 1000, // 5 minutes for search results
247
+ tags: ['search', folder],
248
+ priority: 'high'
249
+ });
250
+ }
251
+ /**
252
+ * Get cached search results
253
+ */
254
+ getCachedSearchResults(query, folder = 'inbox') {
255
+ const cacheKey = this.generateSearchKey(query, folder);
256
+ return this.searchCache.get(cacheKey);
257
+ }
258
+ /**
259
+ * Cache email content
260
+ */
261
+ cacheEmail(email) {
262
+ this.emailCache.set(email.id, email, {
263
+ ttl: 15 * 60 * 1000, // 15 minutes for email content
264
+ tags: ['email', email.from.address],
265
+ priority: 'medium'
266
+ });
267
+ }
268
+ /**
269
+ * Get cached email
270
+ */
271
+ getCachedEmail(emailId) {
272
+ return this.emailCache.get(emailId);
273
+ }
274
+ /**
275
+ * Cache email metadata
276
+ */
277
+ cacheEmailMetadata(emailId, metadata) {
278
+ this.metadataCache.set(`metadata:${emailId}`, metadata, {
279
+ ttl: 30 * 60 * 1000, // 30 minutes for metadata
280
+ tags: ['metadata'],
281
+ priority: 'low'
282
+ });
283
+ }
284
+ /**
285
+ * Get cached email metadata
286
+ */
287
+ getCachedEmailMetadata(emailId) {
288
+ return this.metadataCache.get(`metadata:${emailId}`);
289
+ }
290
+ /**
291
+ * Cache attachment data
292
+ */
293
+ cacheAttachment(messageId, attachmentId, attachmentData) {
294
+ const cacheKey = `attachment:${messageId}:${attachmentId}`;
295
+ this.attachmentCache.set(cacheKey, attachmentData, {
296
+ ttl: 60 * 60 * 1000, // 1 hour for attachments
297
+ tags: ['attachment', messageId],
298
+ priority: 'medium'
299
+ });
300
+ }
301
+ /**
302
+ * Get cached attachment
303
+ */
304
+ getCachedAttachment(messageId, attachmentId) {
305
+ const cacheKey = `attachment:${messageId}:${attachmentId}`;
306
+ return this.attachmentCache.get(cacheKey);
307
+ }
308
+ /**
309
+ * Invalidate caches when emails are modified
310
+ */
311
+ invalidateEmailCaches(emailId) {
312
+ this.emailCache.delete(emailId);
313
+ this.metadataCache.delete(`metadata:${emailId}`);
314
+ this.attachmentCache.clearByTags([emailId]);
315
+ this.searchCache.clearByTags(['search']); // Clear search cache as it might be outdated
316
+ }
317
+ /**
318
+ * Invalidate folder-specific caches
319
+ */
320
+ invalidateFolderCaches(folder) {
321
+ this.searchCache.clearByTags([folder]);
322
+ }
323
+ /**
324
+ * Get comprehensive cache statistics
325
+ */
326
+ getStats() {
327
+ const searchStats = this.searchCache.getStats();
328
+ const emailStats = this.emailCache.getStats();
329
+ const metadataStats = this.metadataCache.getStats();
330
+ const attachmentStats = this.attachmentCache.getStats();
331
+ const totalSize = searchStats.size + emailStats.size + metadataStats.size + attachmentStats.size;
332
+ const totalEntries = searchStats.entries + emailStats.entries + metadataStats.entries + attachmentStats.entries;
333
+ const totalHits = searchStats.hits + emailStats.hits + metadataStats.hits + attachmentStats.hits;
334
+ const totalMisses = searchStats.misses + emailStats.misses + metadataStats.misses + attachmentStats.misses;
335
+ const totalHitRate = totalHits + totalMisses > 0 ? (totalHits / (totalHits + totalMisses)) * 100 : 0;
336
+ return {
337
+ search: searchStats,
338
+ email: emailStats,
339
+ metadata: metadataStats,
340
+ attachment: attachmentStats,
341
+ total: {
342
+ size: totalSize,
343
+ entries: totalEntries,
344
+ hitRate: totalHitRate
345
+ }
346
+ };
347
+ }
348
+ /**
349
+ * Clear all caches
350
+ */
351
+ clearAll() {
352
+ this.searchCache.clear();
353
+ this.emailCache.clear();
354
+ this.metadataCache.clear();
355
+ this.attachmentCache.clear();
356
+ logger.log('🗑️ All caches cleared');
357
+ }
358
+ /**
359
+ * Generate cache key for search
360
+ */
361
+ generateSearchKey(query, folder) {
362
+ // Create a normalized cache key that's case-insensitive and handles variations
363
+ const normalizedQuery = query.toLowerCase().trim().replace(/\s+/g, ' ');
364
+ return `search:${folder}:${normalizedQuery}`;
365
+ }
366
+ /**
367
+ * Optimize cache performance
368
+ */
369
+ optimize() {
370
+ // Force cleanup of expired entries
371
+ this.searchCache['cleanup']();
372
+ this.emailCache['cleanup']();
373
+ this.metadataCache['cleanup']();
374
+ this.attachmentCache['cleanup']();
375
+ logger.log('🔧 Cache optimization completed');
376
+ }
377
+ }
378
+ // Export singleton instance
379
+ export const ms365Cache = MS365CacheManager.getInstance();