UNPKG

bc-symbols-mcp

Version:

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

377 lines 12.1 kB
export class MemoryOptimizedCache { cache; maxAge; maxMemoryMB; compressionEnabled; evictionStrategy; constructor(maxAgeMinutes = 60, maxMemoryMB = 512, compressionEnabled = true, evictionStrategy = 'lru') { this.maxAge = maxAgeMinutes * 60 * 1000; this.maxMemoryMB = maxMemoryMB; this.compressionEnabled = compressionEnabled; this.evictionStrategy = evictionStrategy; this.cache = { apps: {}, tables: {}, codeunits: {}, pages: {}, pageExtensions: {}, reports: {}, enums: {}, others: {} }; } /** * Store app with memory optimization */ setApp(filePath, app) { // Optimize app data before storing const optimizedApp = this.optimizeAppData(app); const memoryUsage = this.calculateObjectMemoryUsage(optimizedApp); this.cache.apps[filePath] = { appData: optimizedApp, timestamp: Date.now(), fileHash: app.fileHash, accessCount: 0, lastAccessed: Date.now(), memoryUsage }; this.enforceMemoryLimits(); } /** * Store objects in type-specific partitions */ setObjects(appId, objectType, objects) { const partition = this.getPartitionForType(objectType); const key = `${appId}:${objectType}`; // Optimize objects before storing const optimizedObjects = this.optimizeObjectsData(objects); if (this.compressionEnabled) { partition[key] = this.compressData(optimizedObjects); } else { partition[key] = optimizedObjects; } this.enforceMemoryLimits(); } /** * Get app with memory tracking */ getApp(filePath, currentFileHash) { const entry = this.cache.apps[filePath]; if (!entry || !this.isEntryValid(entry.timestamp, entry.fileHash, currentFileHash)) { return null; } entry.accessCount++; entry.lastAccessed = Date.now(); return entry.appData; } /** * Get objects from type-specific partition */ getObjects(appId, objectType) { const partition = this.getPartitionForType(objectType); const key = `${appId}:${objectType}`; const data = partition[key]; if (!data) { return null; } if (this.compressionEnabled && this.isCompressed(data)) { return this.decompressData(data); } return data; } /** * Get memory statistics with detailed breakdown */ getMemoryStats() { const stats = { totalMemoryMB: 0, partitionSizes: {}, largestObjects: [], compressionRatio: 1 }; // Calculate partition sizes for (const [partitionName, partition] of Object.entries(this.cache)) { if (partitionName === 'apps') { const appsPartition = partition; const partitionSize = Object.values(appsPartition).reduce((sum, entry) => sum + entry.memoryUsage, 0); stats.partitionSizes[partitionName] = Math.round(partitionSize * 100) / 100; stats.totalMemoryMB += partitionSize; } else { const objectPartition = partition; const partitionSize = this.calculatePartitionMemoryUsage(objectPartition); stats.partitionSizes[partitionName] = Math.round(partitionSize * 100) / 100; stats.totalMemoryMB += partitionSize; } } stats.totalMemoryMB = Math.round(stats.totalMemoryMB * 100) / 100; // Find largest objects stats.largestObjects = this.findLargestObjects().slice(0, 10); // Calculate compression ratio if enabled if (this.compressionEnabled) { stats.compressionRatio = this.calculateCompressionRatio(); } return stats; } /** * Optimize memory usage by removing redundant data */ optimizeAppData(app) { // Create optimized copy with minimal symbol data return { ...app, symbols: { runtimeVersion: app.symbols?.runtimeVersion || '', namespaces: [] // Symbols loaded separately } }; } /** * Optimize objects data by removing unnecessary properties */ optimizeObjectsData(objects) { return objects.map(obj => { // Remove empty arrays and undefined properties const optimized = {}; for (const [key, value] of Object.entries(obj)) { if (value !== undefined && value !== null) { if (Array.isArray(value)) { if (value.length > 0) { optimized[key] = value; } } else if (typeof value === 'object') { const optimizedNested = this.optimizeObjectsData([value])[0]; if (Object.keys(optimizedNested).length > 0) { optimized[key] = optimizedNested; } } else { optimized[key] = value; } } } return optimized; }); } /** * Get appropriate partition for object type */ getPartitionForType(objectType) { switch (objectType.toLowerCase()) { case 'table': case 'tableextension': return this.cache.tables; case 'codeunit': return this.cache.codeunits; case 'page': return this.cache.pages; case 'pageextension': return this.cache.pageExtensions; case 'report': case 'reportextension': return this.cache.reports; case 'enum': return this.cache.enums; default: return this.cache.others; } } /** * Calculate memory usage of an object in MB */ calculateObjectMemoryUsage(obj) { const jsonString = JSON.stringify(obj); return jsonString.length / (1024 * 1024); } /** * Calculate memory usage of a partition */ calculatePartitionMemoryUsage(partition) { let totalSize = 0; for (const data of Object.values(partition)) { totalSize += this.calculateObjectMemoryUsage(data); } return totalSize; } /** * Find largest objects across all partitions */ findLargestObjects() { const largestObjects = []; // Check all partitions for (const [partitionName, partition] of Object.entries(this.cache)) { if (partitionName === 'apps') { const appsPartition = partition; for (const [key, entry] of Object.entries(appsPartition)) { largestObjects.push({ key: `apps:${key}`, sizeMB: entry.memoryUsage }); } } else { const objectPartition = partition; for (const [key, data] of Object.entries(objectPartition)) { largestObjects.push({ key: `${partitionName}:${key}`, sizeMB: this.calculateObjectMemoryUsage(data) }); } } } return largestObjects.sort((a, b) => b.sizeMB - a.sizeMB); } /** * Enforce memory limits using configured eviction strategy */ enforceMemoryLimits() { const currentMemory = this.getMemoryStats().totalMemoryMB; if (currentMemory <= this.maxMemoryMB) { return; } switch (this.evictionStrategy) { case 'lru': this.evictLRU(); break; case 'lfu': this.evictLFU(); break; case 'size-based': this.evictLargest(); break; } } /** * Evict least recently used entries */ evictLRU() { const allEntries = Object.entries(this.cache.apps).map(([key, entry]) => ({ key, lastAccessed: entry.lastAccessed, type: 'app' })); allEntries.sort((a, b) => a.lastAccessed - b.lastAccessed); for (const entry of allEntries) { if (entry.type === 'app') { delete this.cache.apps[entry.key]; } if (this.getMemoryStats().totalMemoryMB <= this.maxMemoryMB) { break; } } } /** * Evict least frequently used entries */ evictLFU() { const allEntries = Object.entries(this.cache.apps).map(([key, entry]) => ({ key, accessCount: entry.accessCount, type: 'app' })); allEntries.sort((a, b) => a.accessCount - b.accessCount); for (const entry of allEntries) { if (entry.type === 'app') { delete this.cache.apps[entry.key]; } if (this.getMemoryStats().totalMemoryMB <= this.maxMemoryMB) { break; } } } /** * Evict largest entries first */ evictLargest() { const largestObjects = this.findLargestObjects(); for (const obj of largestObjects) { const [partitionName, ...keyParts] = obj.key.split(':'); const key = keyParts.join(':'); if (partitionName === 'apps') { delete this.cache.apps[key]; } else { const partition = this.getPartitionForType(partitionName); delete partition[key]; } if (this.getMemoryStats().totalMemoryMB <= this.maxMemoryMB) { break; } } } /** * Check if entry is valid */ isEntryValid(timestamp, entryHash, currentHash) { const now = Date.now(); if (now - timestamp > this.maxAge) { return false; } if (entryHash !== currentHash) { return false; } return true; } /** * Simple compression placeholder */ compressData(data) { // In a real implementation, use a compression library like zlib return { _compressed: true, data: JSON.stringify(data) }; } /** * Simple decompression placeholder */ decompressData(compressedData) { if (compressedData._compressed) { return JSON.parse(compressedData.data); } return compressedData; } /** * Check if data is compressed */ isCompressed(data) { return data && typeof data === 'object' && data._compressed === true; } /** * Calculate compression ratio */ calculateCompressionRatio() { // Placeholder implementation return 0.7; // Assume 30% compression } /** * Clear all cache data */ clear() { this.cache = { apps: {}, tables: {}, codeunits: {}, pages: {}, pageExtensions: {}, reports: {}, enums: {}, others: {} }; } /** * Cleanup expired entries */ cleanup() { const now = Date.now(); let removedCount = 0; // Clean up apps for (const [key, entry] of Object.entries(this.cache.apps)) { if (now - entry.timestamp > this.maxAge) { delete this.cache.apps[key]; removedCount++; } } return removedCount; } } //# sourceMappingURL=memory-optimized-cache.js.map