vibe-coder-mcp
Version:
Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.
328 lines (327 loc) • 11.9 kB
JavaScript
import logger from '../logger.js';
export class TimeoutManager {
static activeTimeouts = new Map();
static completedOperations = new Map();
static isTestEnvironment = false;
static currentEnvironment = 'local';
static config;
static environmentTimeouts = {
local: {
unitTest: 30000,
integrationTest: 120000,
e2eTest: 300000,
setupTimeout: 30000,
teardownTimeout: 20000,
hookTimeout: 15000,
llmRequestTimeout: 60000,
fileOperationTimeout: 10000,
networkOperationTimeout: 30000,
databaseOperationTimeout: 15000,
processStartupTimeout: 60000,
processShutdownTimeout: 30000,
resourceCleanupTimeout: 10000,
warningThreshold: 0.8,
maxRetries: 3,
retryBackoffMs: 1000
},
ci: {
unitTest: 60000,
integrationTest: 180000,
e2eTest: 600000,
setupTimeout: 60000,
teardownTimeout: 45000,
hookTimeout: 30000,
llmRequestTimeout: 120000,
fileOperationTimeout: 20000,
networkOperationTimeout: 60000,
databaseOperationTimeout: 30000,
processStartupTimeout: 120000,
processShutdownTimeout: 60000,
resourceCleanupTimeout: 30000,
warningThreshold: 0.7,
maxRetries: 2,
retryBackoffMs: 2000
},
github: {
unitTest: 90000,
integrationTest: 300000,
e2eTest: 900000,
setupTimeout: 90000,
teardownTimeout: 60000,
hookTimeout: 45000,
llmRequestTimeout: 180000,
fileOperationTimeout: 30000,
networkOperationTimeout: 90000,
databaseOperationTimeout: 45000,
processStartupTimeout: 180000,
processShutdownTimeout: 90000,
resourceCleanupTimeout: 45000,
warningThreshold: 0.6,
maxRetries: 1,
retryBackoffMs: 3000
},
docker: {
unitTest: 45000,
integrationTest: 240000,
e2eTest: 720000,
setupTimeout: 75000,
teardownTimeout: 45000,
hookTimeout: 30000,
llmRequestTimeout: 150000,
fileOperationTimeout: 25000,
networkOperationTimeout: 60000,
databaseOperationTimeout: 30000,
processStartupTimeout: 150000,
processShutdownTimeout: 75000,
resourceCleanupTimeout: 30000,
warningThreshold: 0.75,
maxRetries: 2,
retryBackoffMs: 1500
}
};
static initialize(customConfig) {
if (process.env.NODE_ENV !== 'test' && !process.env.VITEST) {
logger.warn('TimeoutManager should only be used in test environment');
return;
}
this.isTestEnvironment = true;
this.detectEnvironment();
this.config = { ...this.environmentTimeouts[this.currentEnvironment] };
if (customConfig) {
this.config = { ...this.config, ...customConfig };
}
logger.debug({
environment: this.currentEnvironment,
config: this.config
}, 'TimeoutManager initialized');
}
static detectEnvironment() {
if (process.env.GITHUB_ACTIONS === 'true') {
this.currentEnvironment = 'github';
}
else if (process.env.CI === 'true') {
this.currentEnvironment = 'ci';
}
else if (process.env.DOCKER === 'true' || process.cwd().includes('docker')) {
this.currentEnvironment = 'docker';
}
else {
this.currentEnvironment = 'local';
}
logger.debug({ environment: this.currentEnvironment }, 'Test environment detected');
}
static startOperation(operationId, operationType, customTimeoutMs, onTimeout) {
if (!this.isTestEnvironment) {
return;
}
this.clearOperation(operationId);
const timeoutMs = customTimeoutMs || this.config[operationType];
const startTime = Date.now();
const warningTimeoutMs = timeoutMs * this.config.warningThreshold;
const warningId = setTimeout(() => {
const elapsed = Date.now() - startTime;
logger.warn({
operationId,
operationType,
elapsed,
timeout: timeoutMs,
remainingMs: timeoutMs - elapsed
}, 'Operation timeout warning - approaching limit');
}, warningTimeoutMs);
const timeoutId = setTimeout(() => {
const duration = Date.now() - startTime;
const result = {
operationId,
startTime,
endTime: Date.now(),
duration,
timedOut: true,
warnings: [`Operation timed out after ${duration}ms (limit: ${timeoutMs}ms)`],
errorMessage: `Operation '${operationId}' (${operationType}) timed out after ${duration}ms`
};
this.completedOperations.set(operationId, result);
this.activeTimeouts.delete(operationId);
logger.error({
operationId,
operationType,
duration,
timeoutMs
}, 'Operation timed out');
if (onTimeout) {
try {
onTimeout();
}
catch (error) {
logger.error({ err: error, operationId }, 'Error in timeout handler');
}
}
}, timeoutMs);
this.activeTimeouts.set(operationId, {
operationId,
timeoutId,
startTime,
timeoutMs,
operationType,
warningId,
onTimeout
});
logger.debug({
operationId,
operationType,
timeoutMs,
warningThreshold: this.config.warningThreshold
}, 'Operation timeout monitoring started');
}
static completeOperation(operationId, success = true) {
if (!this.isTestEnvironment) {
return null;
}
const activeTimeout = this.activeTimeouts.get(operationId);
if (!activeTimeout) {
logger.debug({ operationId }, 'No active timeout found for operation');
return null;
}
clearTimeout(activeTimeout.timeoutId);
if (activeTimeout.warningId) {
clearTimeout(activeTimeout.warningId);
}
const endTime = Date.now();
const duration = endTime - activeTimeout.startTime;
const result = {
operationId,
startTime: activeTimeout.startTime,
endTime,
duration,
timedOut: false,
warnings: []
};
const slowThreshold = activeTimeout.timeoutMs * 0.5;
if (duration > slowThreshold) {
result.warnings.push(`Operation took ${duration}ms (${Math.round(duration / activeTimeout.timeoutMs * 100)}% of timeout limit)`);
}
if (!success) {
result.errorMessage = `Operation '${operationId}' failed after ${duration}ms`;
}
this.completedOperations.set(operationId, result);
this.activeTimeouts.delete(operationId);
logger.debug({
operationId,
duration,
success,
warnings: result.warnings.length
}, 'Operation completed');
return result;
}
static clearOperation(operationId) {
if (!this.isTestEnvironment) {
return;
}
const activeTimeout = this.activeTimeouts.get(operationId);
if (activeTimeout) {
clearTimeout(activeTimeout.timeoutId);
if (activeTimeout.warningId) {
clearTimeout(activeTimeout.warningId);
}
this.activeTimeouts.delete(operationId);
logger.debug({ operationId }, 'Operation timeout cleared');
}
}
static getTimeout(operationType) {
return this.config[operationType];
}
static async withTimeout(operationId, operationType, operation, customTimeoutMs) {
if (!this.isTestEnvironment) {
return operation();
}
return new Promise((resolve, reject) => {
let completed = false;
this.startOperation(operationId, operationType, customTimeoutMs, () => {
if (!completed) {
completed = true;
reject(new Error(`Operation '${operationId}' timed out after ${customTimeoutMs || this.config[operationType]}ms`));
}
});
Promise.resolve(operation())
.then((result) => {
if (!completed) {
completed = true;
this.completeOperation(operationId, true);
resolve(result);
}
})
.catch((error) => {
if (!completed) {
completed = true;
this.completeOperation(operationId, false);
reject(error);
}
});
});
}
static getStats() {
const completed = Array.from(this.completedOperations.values());
const timedOut = completed.filter(op => op.timedOut);
const successful = completed.filter(op => !op.timedOut && op.duration);
const avgDuration = successful.length > 0
? successful.reduce((sum, op) => sum + (op.duration || 0), 0) / successful.length
: 0;
return {
activeOperations: this.activeTimeouts.size,
completedOperations: this.completedOperations.size,
timedOutOperations: timedOut.length,
averageDuration: Math.round(avgDuration),
currentEnvironment: this.currentEnvironment,
config: { ...this.config }
};
}
static getOperationReport(operationId) {
return this.completedOperations.get(operationId) || null;
}
static getAllReports() {
return Array.from(this.completedOperations.values());
}
static reset() {
if (!this.isTestEnvironment) {
return;
}
for (const activeTimeout of this.activeTimeouts.values()) {
clearTimeout(activeTimeout.timeoutId);
if (activeTimeout.warningId) {
clearTimeout(activeTimeout.warningId);
}
}
this.activeTimeouts.clear();
this.completedOperations.clear();
logger.debug('TimeoutManager reset');
}
static updateConfig(newConfig) {
if (!this.isTestEnvironment) {
return;
}
this.config = { ...this.config, ...newConfig };
logger.debug({ config: this.config }, 'TimeoutManager configuration updated');
}
static getCurrentEnvironment() {
return this.currentEnvironment;
}
static forceEnvironment(environment) {
if (!this.isTestEnvironment) {
return;
}
this.currentEnvironment = environment;
this.config = { ...this.environmentTimeouts[environment] };
logger.debug({ environment }, 'Environment forced');
}
}
export function initializeTimeoutManager(config) {
TimeoutManager.initialize(config);
}
export async function withTimeout(operationId, operationType, operation, customTimeoutMs) {
return TimeoutManager.withTimeout(operationId, operationType, operation, customTimeoutMs);
}
export function getTimeout(operationType) {
return TimeoutManager.getTimeout(operationType);
}
export function getTimeoutStats() {
return TimeoutManager.getStats();
}