UNPKG

bc-symbols-mcp

Version:

MCP server for analyzing Business Central .app files and BC object structures

233 lines 8.04 kB
import { StreamingSymbolParser } from '../processors/streaming-parser.js'; export class LazyMemoryCache { cache = {}; maxAge; maxMemoryMB; accessLog = { hits: 0, misses: 0 }; constructor(maxAgeMinutes = 60, maxMemoryMB = 512) { this.maxAge = maxAgeMinutes * 60 * 1000; this.maxMemoryMB = maxMemoryMB; } /** * Store an app with lazy loading capabilities */ async setLazy(filePath, app, symbolsBuffer) { const streamingParser = new StreamingSymbolParser(); await streamingParser.parseSymbolsProgressive(symbolsBuffer); this.cache[filePath] = { appData: app, symbolsBuffer, streamingParser, objectCache: {}, timestamp: Date.now(), fileHash: app.fileHash, accessCount: 0, lastAccessed: Date.now() }; // Check memory limits and evict if necessary await this.enforceMemoryLimits(); } /** * Get app metadata without loading symbols */ getAppMetadata(filePath, currentFileHash) { const entry = this.cache[filePath]; if (!entry || !this.isEntryValid(entry, currentFileHash)) { return null; } entry.accessCount++; entry.lastAccessed = Date.now(); return entry.appData; } /** * Load objects of specific type on demand */ async loadObjectsByType(filePath, currentFileHash, objectType) { const entry = this.cache[filePath]; if (!entry || !this.isEntryValid(entry, currentFileHash)) { this.accessLog.misses++; return []; } const cacheKey = `${objectType}:all`; // Check if objects are already cached if (entry.objectCache[cacheKey]) { this.accessLog.hits++; entry.lastAccessed = Date.now(); return entry.objectCache[cacheKey]; } // Load objects using streaming parser const objects = await entry.streamingParser.loadObjectsByType(objectType); entry.objectCache[cacheKey] = objects; entry.lastAccessed = Date.now(); this.accessLog.misses++; return objects; } /** * Load specific object on demand */ async loadObject(filePath, currentFileHash, objectType, objectId, objectName) { const entry = this.cache[filePath]; if (!entry || !this.isEntryValid(entry, currentFileHash)) { this.accessLog.misses++; return null; } const cacheKey = `${objectType}:${objectId}:${objectName}`; // Check if object is already cached if (entry.objectCache[cacheKey]) { this.accessLog.hits++; entry.lastAccessed = Date.now(); return entry.objectCache[cacheKey]; } // Load object using streaming parser try { const object = await entry.streamingParser.loadObject(objectType, objectId, objectName); entry.objectCache[cacheKey] = object; entry.lastAccessed = Date.now(); this.accessLog.misses++; return object; } catch (error) { this.accessLog.misses++; return null; } } /** * Get object metadata without loading full object */ getObjectMetadata(filePath, currentFileHash, objectType, objectId, objectName) { const entry = this.cache[filePath]; if (!entry || !this.isEntryValid(entry, currentFileHash)) { return []; } entry.lastAccessed = Date.now(); return entry.streamingParser.getObjectMetadata(objectType, objectId, objectName); } /** * Get loading statistics for an app */ getLoadingStats(filePath, currentFileHash) { const entry = this.cache[filePath]; if (!entry || !this.isEntryValid(entry, currentFileHash)) { return null; } return entry.streamingParser.getLoadingStats(); } /** * Get comprehensive cache statistics */ getCacheStats() { const totalObjects = Object.values(this.cache).reduce((sum, entry) => sum + Object.keys(entry.objectCache).length, 0); const loadedObjects = Object.values(this.cache).reduce((sum, entry) => sum + entry.streamingParser.getLoadingStats().loaded, 0); const memoryUsage = this.calculateMemoryUsage(); const totalRequests = this.accessLog.hits + this.accessLog.misses; const cacheHitRate = totalRequests > 0 ? (this.accessLog.hits / totalRequests) * 100 : 0; return { totalApps: Object.keys(this.cache).length, totalObjects, loadedObjects, memoryUsage, cacheHitRate: Math.round(cacheHitRate * 100) / 100, averageLoadTime: 0 // Would need to track timing data }; } /** * Check if entry is valid (not expired and hash matches) */ isEntryValid(entry, currentFileHash) { const now = Date.now(); if (now - entry.timestamp > this.maxAge) { return false; } if (entry.fileHash !== currentFileHash) { return false; } return true; } /** * Calculate approximate memory usage in MB */ calculateMemoryUsage() { let totalBytes = 0; for (const entry of Object.values(this.cache)) { // Rough estimation of memory usage totalBytes += entry.symbolsBuffer.length; totalBytes += JSON.stringify(entry.appData).length; totalBytes += JSON.stringify(entry.objectCache).length; } return Math.round((totalBytes / (1024 * 1024)) * 100) / 100; } /** * Enforce memory limits using LRU eviction */ async enforceMemoryLimits() { while (this.calculateMemoryUsage() > this.maxMemoryMB) { // Find least recently used entry let oldestEntry = null; let oldestTime = Date.now(); for (const [filePath, entry] of Object.entries(this.cache)) { if (entry.lastAccessed < oldestTime) { oldestTime = entry.lastAccessed; oldestEntry = filePath; } } if (oldestEntry) { delete this.cache[oldestEntry]; } else { break; // Safety break } } } /** * Clean up expired entries */ cleanup() { const now = Date.now(); let removedCount = 0; for (const [filePath, entry] of Object.entries(this.cache)) { if (now - entry.timestamp > this.maxAge) { delete this.cache[filePath]; removedCount++; } } return removedCount; } /** * Clear all cached data */ clear() { this.cache = {}; this.accessLog = { hits: 0, misses: 0 }; } /** * Get all cached app file paths */ getCachedPaths() { return Object.keys(this.cache); } /** * Check if an app is cached */ has(filePath, currentFileHash) { const entry = this.cache[filePath]; return entry ? this.isEntryValid(entry, currentFileHash) : false; } /** * Pre-load objects for better performance */ async preloadObjects(filePath, currentFileHash, objectTypes) { const entry = this.cache[filePath]; if (!entry || !this.isEntryValid(entry, currentFileHash)) { return; } // Pre-load specified object types in background const preloadPromises = objectTypes.map(async (objectType) => { const cacheKey = `${objectType}:all`; if (!entry.objectCache[cacheKey]) { entry.objectCache[cacheKey] = await entry.streamingParser.loadObjectsByType(objectType); } }); await Promise.all(preloadPromises); } } //# sourceMappingURL=lazy-cache.js.map