ai-debug-local-mcp
Version:
🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh
306 lines • 10.7 kB
JavaScript
// Resource Management System
// Provides consistent browser resource tracking and cleanup across all handlers
/**
* ResourceManager - Centralized browser resource management
*
* Provides consistent resource tracking and cleanup across all handlers
* to prevent memory leaks and ensure proper resource disposal
*/
export class ResourceManager {
resources = new Map();
cleanupCallbacks = [];
/**
* Track a resource for automatic cleanup
*/
trackResource(resourceType, sessionId, resource, addedAt) {
const resourceId = `${resourceType}-${sessionId}-${Date.now()}`;
this.resources.set(resourceId, {
sessionId,
resourceType,
resource,
addedAt: addedAt || new Date(),
lastUsed: new Date()
});
}
/**
* Track a resource with handler information
*/
trackHandlerResource(handlerName, sessionId, resource) {
const resourceId = `handler-${handlerName}-${sessionId}-${Date.now()}`;
this.resources.set(resourceId, {
sessionId,
resourceType: 'page', // Most handlers use pages
resource,
handlerName,
addedAt: new Date(),
lastUsed: new Date()
});
}
/**
* Get total number of tracked resources
*/
getResourceCount() {
return this.resources.size;
}
/**
* Get detailed resource statistics
*/
getResourceStats() {
const resourceTypes = {};
const handlerUsage = {};
const sessions = new Set();
for (const resource of this.resources.values()) {
resourceTypes[resource.resourceType] = (resourceTypes[resource.resourceType] || 0) + 1;
sessions.add(resource.sessionId);
if (resource.handlerName) {
handlerUsage[resource.handlerName] = (handlerUsage[resource.handlerName] || 0) + 1;
}
}
return {
totalResources: this.resources.size,
resourceTypes,
sessionCount: sessions.size,
handlerUsage
};
}
/**
* Detect resource leaks (resources older than threshold)
*/
detectResourceLeaks(ageThresholdMinutes = 10) {
const leaks = [];
const now = Date.now();
const threshold = ageThresholdMinutes * 60 * 1000;
for (const resource of this.resources.values()) {
const age = now - resource.addedAt.getTime();
if (age > threshold) {
leaks.push({
sessionId: resource.sessionId,
resourceType: resource.resourceType,
age: Math.floor(age / 1000 / 60), // age in minutes
handlerName: resource.handlerName
});
}
}
return leaks;
}
/**
* Detect resource inconsistencies
*/
detectInconsistencies() {
const inconsistencies = [];
for (const resource of this.resources.values()) {
try {
// Check if page is closed but still tracked
if (resource.resourceType === 'page' && resource.resource.isClosed && resource.resource.isClosed()) {
inconsistencies.push({
sessionId: resource.sessionId,
resourceType: resource.resourceType,
issue: 'tracked-but-closed',
resource: resource.resource
});
}
// Check if browser is disconnected but still tracked
if (resource.resourceType === 'browser' && resource.resource.isConnected && !resource.resource.isConnected()) {
inconsistencies.push({
sessionId: resource.sessionId,
resourceType: resource.resourceType,
issue: 'tracked-but-disconnected',
resource: resource.resource
});
}
}
catch (error) {
// Resource might be disposed, mark as inconsistent
inconsistencies.push({
sessionId: resource.sessionId,
resourceType: resource.resourceType,
issue: 'resource-disposed',
resource: resource.resource
});
}
}
return inconsistencies;
}
/**
* Fix detected inconsistencies
*/
async fixInconsistencies() {
const inconsistencies = this.detectInconsistencies();
for (const inconsistency of inconsistencies) {
// Remove inconsistent resources from tracking
const toRemove = [];
for (const [id, resource] of this.resources.entries()) {
if (resource.sessionId === inconsistency.sessionId &&
resource.resourceType === inconsistency.resourceType &&
resource.resource === inconsistency.resource) {
toRemove.push(id);
}
}
toRemove.forEach(id => this.resources.delete(id));
}
}
/**
* Get monitoring data for analysis
*/
getMonitoringData() {
const stats = this.getResourceStats();
const resourceAge = {};
// Calculate average age by resource type
const agesByType = {};
for (const resource of this.resources.values()) {
if (!agesByType[resource.resourceType]) {
agesByType[resource.resourceType] = [];
}
agesByType[resource.resourceType].push(Date.now() - resource.addedAt.getTime());
}
for (const [type, ages] of Object.entries(agesByType)) {
resourceAge[type] = ages.reduce((sum, age) => sum + age, 0) / ages.length;
}
return {
totalResources: stats.totalResources,
resourceAge,
sessionCount: stats.sessionCount,
averageResourcesPerSession: stats.sessionCount > 0 ? stats.totalResources / stats.sessionCount : 0,
handlerUsage: stats.handlerUsage
};
}
/**
* Get cleanup recommendations
*/
getCleanupRecommendations() {
const stats = this.getResourceStats();
const leaks = this.detectResourceLeaks();
const inconsistencies = this.detectInconsistencies();
const recommendedActions = [];
if (leaks.length > 0) {
recommendedActions.push({
action: 'cleanup-leaks',
priority: 'high',
description: `${leaks.length} resource leaks detected - immediate cleanup recommended`
});
}
if (inconsistencies.length > 0) {
recommendedActions.push({
action: 'fix-inconsistencies',
priority: 'medium',
description: `${inconsistencies.length} resource inconsistencies detected`
});
}
if (stats.totalResources > 50) {
recommendedActions.push({
action: 'bulk-cleanup',
priority: 'medium',
description: 'High resource count - consider bulk cleanup'
});
}
const priority = leaks.length > 0 ? 'high' : inconsistencies.length > 0 ? 'medium' : 'low';
return {
totalTracked: stats.totalResources,
recommendedActions,
priority
};
}
/**
* Cleanup resources for a specific session
*/
async cleanupSession(sessionId) {
const sessionResources = [];
// Find all resources for this session
for (const [id, resource] of this.resources.entries()) {
if (resource.sessionId === sessionId) {
sessionResources.push(id);
}
}
// Cleanup each resource
for (const resourceId of sessionResources) {
const resource = this.resources.get(resourceId);
if (resource) {
await this.cleanupResource(resource);
this.resources.delete(resourceId);
}
}
// Call registered cleanup callbacks
for (const callback of this.cleanupCallbacks) {
try {
await callback(sessionId);
}
catch (error) {
console.warn(`Cleanup callback failed for session ${sessionId}:`, error);
}
}
}
/**
* Cleanup all resources
*/
async cleanupAll() {
const sessions = new Set();
// Get all unique session IDs
for (const resource of this.resources.values()) {
sessions.add(resource.sessionId);
}
// Cleanup each session
for (const sessionId of sessions) {
await this.cleanupSession(sessionId);
}
}
/**
* Cleanup individual resource
*/
async cleanupResource(resource) {
try {
if (resource.resourceType === 'page' && resource.resource.close) {
await resource.resource.close();
}
else if (resource.resourceType === 'browser' && resource.resource.close) {
await resource.resource.close();
}
else if (resource.resourceType === 'context' && resource.resource.close) {
await resource.resource.close();
}
}
catch (error) {
console.warn(`Failed to cleanup ${resource.resourceType} resource:`, error);
}
}
/**
* Register cleanup callback
*/
registerCleanupCallback(callback) {
this.cleanupCallbacks.push(callback);
}
/**
* Enhance existing cleanup function
*/
enhanceCleanup(originalCleanup) {
return async (sessionId) => {
// Run resource manager cleanup first
await this.cleanupSession(sessionId);
// Then run original cleanup
await originalCleanup(sessionId);
};
}
/**
* Integration with main server
*/
integrateWithServer() {
// This method would integrate with the main server's cleanup system
// For now, it's a placeholder for the integration point
}
/**
* Clear all resources (for testing)
*/
clear() {
this.resources.clear();
}
/**
* Generic cleanup method - maps to cleanupAll for compatibility
*/
async cleanup() {
await this.cleanupAll();
}
}
/**
* Global resource manager instance
*/
export const globalResourceManager = new ResourceManager();
//# sourceMappingURL=resource-manager.js.map