bc-symbols-mcp
Version:
MCP server for analyzing Business Central .app files and BC object structures
274 lines • 10.5 kB
JavaScript
import { EventEmitter } from 'events';
import { OptimizedAppExtractor } from './app-extractor-optimized.js';
export class ProgressiveAppLoader extends EventEmitter {
extractor;
loadingTasks = new Map();
loadingProgress = new Map();
constructor(extractor) {
super();
this.extractor = extractor || new OptimizedAppExtractor();
}
/**
* Load app with progressive loading and progress reporting
*/
async loadAppProgressive(filePath, options = {}) {
const defaultOptions = {
batchSize: 50,
delayBetweenBatches: 10,
priorityObjectTypes: ['table', 'codeunit', 'page'],
backgroundLoadingEnabled: true,
...options
};
const loadingId = `${filePath}-${Date.now()}`;
try {
// Phase 1: Load metadata and manifest
this.updateProgress(loadingId, {
phase: 'metadata',
percentage: 10,
message: 'Loading app metadata...',
objectsLoaded: 0,
totalObjects: 0
}, defaultOptions.progressCallback);
const app = await this.extractor.extractAppLazy(filePath);
// Phase 2: Build object index
this.updateProgress(loadingId, {
phase: 'symbols',
percentage: 25,
message: 'Building object index...',
objectsLoaded: 0,
totalObjects: 0
}, defaultOptions.progressCallback);
const loadingStats = this.extractor.getLoadingStats(filePath);
const totalObjects = loadingStats?.total || 0;
// Phase 3: Progressive object loading
this.updateProgress(loadingId, {
phase: 'objects',
percentage: 30,
message: 'Loading objects progressively...',
objectsLoaded: 0,
totalObjects
}, defaultOptions.progressCallback);
if (defaultOptions.backgroundLoadingEnabled) {
// Start background loading
this.startBackgroundLoading(filePath, defaultOptions, loadingId);
}
else {
// Load all objects synchronously
await this.loadAllObjectsSync(filePath, defaultOptions, loadingId, totalObjects);
}
// Phase 4: Complete
this.updateProgress(loadingId, {
phase: 'complete',
percentage: 100,
message: 'Loading complete',
objectsLoaded: totalObjects,
totalObjects
}, defaultOptions.progressCallback);
return app;
}
catch (error) {
this.emit('error', { loadingId, error });
throw error;
}
finally {
this.loadingProgress.delete(loadingId);
}
}
/**
* Start background loading of objects
*/
async startBackgroundLoading(filePath, options, loadingId) {
const taskId = `bg-${loadingId}`;
const loadingTask = this.loadObjectsInBackground(filePath, options, loadingId);
this.loadingTasks.set(taskId, loadingTask);
// Don't await - let it run in background
loadingTask.finally(() => {
this.loadingTasks.delete(taskId);
});
}
/**
* Load objects in the background with progress updates
*/
async loadObjectsInBackground(filePath, options, loadingId) {
const startTime = Date.now();
let loadedCount = 0;
try {
// Get all object metadata first
const allMetadata = this.getAllObjectMetadata(filePath);
const totalObjects = allMetadata.length;
// Group by priority
const priorityObjects = allMetadata.filter(meta => options.priorityObjectTypes.includes(meta.objectType));
const otherObjects = allMetadata.filter(meta => !options.priorityObjectTypes.includes(meta.objectType));
// Load priority objects first
loadedCount += await this.loadObjectBatches(filePath, priorityObjects, options, loadingId, loadedCount, totalObjects, startTime);
// Load remaining objects
await this.loadObjectBatches(filePath, otherObjects, options, loadingId, loadedCount, totalObjects, startTime);
this.emit('backgroundLoadingComplete', { filePath, loadingId });
}
catch (error) {
this.emit('backgroundLoadingError', { filePath, loadingId, error });
}
}
/**
* Load objects in batches with delays
*/
async loadObjectBatches(filePath, objects, options, loadingId, initialLoadedCount, totalObjects, startTime) {
let loadedCount = initialLoadedCount;
for (let i = 0; i < objects.length; i += options.batchSize) {
const batch = objects.slice(i, i + options.batchSize);
const currentObjectType = batch[0]?.objectType;
// Load batch
const loadPromises = batch.map(meta => this.extractor.loadObject(filePath, meta.objectType, meta.objectId, meta.name));
await Promise.all(loadPromises);
loadedCount += batch.length;
// Calculate progress and ETA
const elapsedTime = Date.now() - startTime;
const objectsPerMs = loadedCount / elapsedTime;
const remainingObjects = totalObjects - loadedCount;
const estimatedTimeRemaining = objectsPerMs > 0 ? remainingObjects / objectsPerMs : 0;
// Update progress
const percentage = 30 + (loadedCount / totalObjects) * 65; // 30-95% range
this.updateProgress(loadingId, {
phase: 'objects',
percentage: Math.min(percentage, 95),
message: `Loading ${currentObjectType} objects...`,
objectsLoaded: loadedCount,
totalObjects,
currentObjectType,
estimatedTimeRemaining
}, options.progressCallback);
// Emit batch completion event
this.emit('batchLoaded', {
filePath,
loadingId,
batchSize: batch.length,
totalLoaded: loadedCount,
totalObjects
});
// Add delay between batches to prevent blocking
if (i + options.batchSize < objects.length && options.delayBetweenBatches > 0) {
await this.delay(options.delayBetweenBatches);
}
}
return loadedCount;
}
/**
* Load all objects synchronously (for non-background mode)
*/
async loadAllObjectsSync(filePath, options, loadingId, totalObjects) {
const allMetadata = this.getAllObjectMetadata(filePath);
await this.loadObjectBatches(filePath, allMetadata, options, loadingId, 0, totalObjects, Date.now());
}
/**
* Get all object metadata for progressive loading
*/
getAllObjectMetadata(filePath) {
const objectTypes = ['table', 'codeunit', 'page', 'pageextension', 'report', 'enum'];
const allMetadata = [];
for (const objectType of objectTypes) {
const metadata = this.extractor.getObjectMetadata(filePath, objectType);
allMetadata.push(...metadata);
}
return allMetadata;
}
/**
* Update loading progress
*/
updateProgress(loadingId, progress, callback) {
this.loadingProgress.set(loadingId, progress);
if (callback) {
callback(progress);
}
this.emit('progress', { loadingId, progress });
}
/**
* Get current loading progress
*/
getLoadingProgress(loadingId) {
return this.loadingProgress.get(loadingId) || null;
}
/**
* Get all active loading tasks
*/
getActiveLoadingTasks() {
return Array.from(this.loadingTasks.keys());
}
/**
* Cancel background loading task
*/
cancelBackgroundLoading(taskId) {
const task = this.loadingTasks.get(taskId);
if (task) {
this.loadingTasks.delete(taskId);
return true;
}
return false;
}
/**
* Wait for specific loading task to complete
*/
async waitForLoadingTask(taskId) {
const task = this.loadingTasks.get(taskId);
if (task) {
return await task;
}
throw new Error(`Loading task ${taskId} not found`);
}
/**
* Preload specific object types
*/
async preloadObjectTypes(filePath, objectTypes, progressCallback) {
const loadingId = `preload-${filePath}-${Date.now()}`;
let totalLoaded = 0;
let totalObjects = 0;
// Calculate total objects to load
for (const objectType of objectTypes) {
const metadata = this.extractor.getObjectMetadata(filePath, objectType);
totalObjects += metadata.length;
}
for (const objectType of objectTypes) {
const metadata = this.extractor.getObjectMetadata(filePath, objectType);
this.updateProgress(loadingId, {
phase: 'objects',
percentage: (totalLoaded / totalObjects) * 100,
message: `Preloading ${objectType} objects...`,
objectsLoaded: totalLoaded,
totalObjects,
currentObjectType: objectType
}, progressCallback);
await this.extractor.loadObjectsByType(filePath, objectType);
totalLoaded += metadata.length;
}
this.updateProgress(loadingId, {
phase: 'complete',
percentage: 100,
message: 'Preloading complete',
objectsLoaded: totalObjects,
totalObjects
}, progressCallback);
}
/**
* Utility delay function
*/
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Get cache statistics
*/
getCacheStats() {
return this.extractor.getCacheStats();
}
/**
* Cleanup resources
*/
cleanup() {
// Cancel all active loading tasks
for (const taskId of this.loadingTasks.keys()) {
this.loadingTasks.delete(taskId);
}
this.loadingProgress.clear();
this.removeAllListeners();
}
}
//# sourceMappingURL=progressive-loader.js.map