claude-flow-tbowman01
Version:
Enterprise-grade AI agent orchestration with ruv-swarm integration (Alpha Release)
318 lines • 11 kB
JavaScript
/**
* Connection State Manager for MCP
* Persists connection state across disconnections
*/
import { promises as fs } from 'node:fs';
import { join } from 'node:path';
export class ConnectionStateManager {
logger;
currentState;
connectionHistory = [];
metrics = {
totalConnections: 0,
totalDisconnections: 0,
totalReconnections: 0,
averageSessionDuration: 0,
averageReconnectionTime: 0,
connectionHistory: [],
};
persistenceTimer;
statePath;
metricsPath;
defaultConfig = {
enablePersistence: true,
stateDirectory: '.mcp-state',
maxHistorySize: 1000,
persistenceInterval: 60000, // 1 minute
};
constructor(logger, config) {
this.logger = logger;
this.config = { ...this.defaultConfig, ...config };
this.statePath = join(this.config.stateDirectory, 'connection-state.json');
this.metricsPath = join(this.config.stateDirectory, 'connection-metrics.json');
this.initialize().catch((error) => {
this.logger.error('Failed to initialize state manager', error);
});
}
config;
/**
* Initialize the state manager
*/
async initialize() {
if (!this.config.enablePersistence) {
return;
}
try {
// Ensure state directory exists
await fs.mkdir(this.config.stateDirectory, { recursive: true });
// Load existing state
await this.loadState();
await this.loadMetrics();
// Start persistence timer
this.startPersistenceTimer();
this.logger.info('Connection state manager initialized', {
stateDirectory: this.config.stateDirectory,
});
}
catch (error) {
this.logger.error('Failed to initialize state manager', error);
}
}
/**
* Save current connection state
*/
saveState(state) {
this.currentState = {
...state,
metadata: {
...state.metadata,
lastSaved: new Date().toISOString(),
},
};
this.logger.debug('Connection state saved', {
sessionId: state.sessionId,
pendingRequests: state.pendingRequests.length,
});
// Persist immediately if critical
if (state.pendingRequests.length > 0) {
this.persistState().catch((error) => {
this.logger.error('Failed to persist critical state', error);
});
}
}
/**
* Restore previous connection state
*/
restoreState() {
if (!this.currentState) {
this.logger.debug('No state to restore');
return null;
}
this.logger.info('Restoring connection state', {
sessionId: this.currentState.sessionId,
pendingRequests: this.currentState.pendingRequests.length,
});
return { ...this.currentState };
}
/**
* Record a connection event
*/
recordEvent(event) {
const fullEvent = {
...event,
timestamp: new Date(),
};
this.connectionHistory.push(fullEvent);
// Trim history if needed
if (this.connectionHistory.length > this.config.maxHistorySize) {
this.connectionHistory = this.connectionHistory.slice(-this.config.maxHistorySize);
}
// Update metrics
this.updateMetrics(fullEvent);
this.logger.debug('Connection event recorded', {
type: event.type,
sessionId: event.sessionId,
});
}
/**
* Get connection metrics
*/
getMetrics() {
return {
...this.metrics,
connectionHistory: [...this.connectionHistory],
};
}
/**
* Clear a specific session state
*/
clearSession(sessionId) {
if (this.currentState?.sessionId === sessionId) {
this.currentState = undefined;
this.logger.info('Session state cleared', { sessionId });
this.persistState().catch((error) => {
this.logger.error('Failed to persist cleared state', error);
});
}
}
/**
* Add a pending request
*/
addPendingRequest(request) {
if (!this.currentState) {
this.logger.warn('No active state to add pending request');
return;
}
this.currentState.pendingRequests.push(request);
this.logger.debug('Pending request added', {
requestId: request.id,
method: request.method,
total: this.currentState.pendingRequests.length,
});
}
/**
* Remove a pending request
*/
removePendingRequest(requestId) {
if (!this.currentState) {
return;
}
this.currentState.pendingRequests = this.currentState.pendingRequests.filter((req) => req.id !== requestId);
}
/**
* Get pending requests
*/
getPendingRequests() {
return this.currentState?.pendingRequests || [];
}
/**
* Update session metadata
*/
updateMetadata(metadata) {
if (!this.currentState) {
return;
}
this.currentState.metadata = {
...this.currentState.metadata,
...metadata,
};
}
/**
* Calculate session duration
*/
getSessionDuration(sessionId) {
const connectEvent = this.connectionHistory.find((e) => e.sessionId === sessionId && e.type === 'connect');
const disconnectEvent = this.connectionHistory.find((e) => e.sessionId === sessionId && e.type === 'disconnect');
if (!connectEvent) {
return null;
}
const endTime = disconnectEvent ? disconnectEvent.timestamp : new Date();
return endTime.getTime() - connectEvent.timestamp.getTime();
}
/**
* Get reconnection time for a session
*/
getReconnectionTime(sessionId) {
const disconnectEvent = this.connectionHistory.find((e) => e.sessionId === sessionId && e.type === 'disconnect');
const reconnectEvent = this.connectionHistory.find((e) => e.sessionId === sessionId &&
e.type === 'reconnect' &&
e.timestamp > (disconnectEvent?.timestamp || new Date(0)));
if (!disconnectEvent || !reconnectEvent) {
return null;
}
return reconnectEvent.timestamp.getTime() - disconnectEvent.timestamp.getTime();
}
updateMetrics(event) {
switch (event.type) {
case 'connect':
this.metrics.totalConnections++;
break;
case 'disconnect':
this.metrics.totalDisconnections++;
// Calculate session duration
const duration = this.getSessionDuration(event.sessionId);
if (duration !== null) {
this.metrics.lastConnectionDuration = duration;
// Update average
const totalDuration = this.metrics.averageSessionDuration * (this.metrics.totalDisconnections - 1) + duration;
this.metrics.averageSessionDuration = totalDuration / this.metrics.totalDisconnections;
}
break;
case 'reconnect':
this.metrics.totalReconnections++;
// Calculate reconnection time
const reconnectTime = this.getReconnectionTime(event.sessionId);
if (reconnectTime !== null) {
// Update average
const totalTime = this.metrics.averageReconnectionTime * (this.metrics.totalReconnections - 1) +
reconnectTime;
this.metrics.averageReconnectionTime = totalTime / this.metrics.totalReconnections;
}
break;
}
}
async loadState() {
try {
const data = await fs.readFile(this.statePath, 'utf-8');
const state = JSON.parse(data);
// Convert date strings back to Date objects
state.lastConnected = new Date(state.lastConnected);
if (state.lastDisconnected) {
state.lastDisconnected = new Date(state.lastDisconnected);
}
this.currentState = state;
this.logger.info('Connection state loaded', {
sessionId: state.sessionId,
pendingRequests: state.pendingRequests.length,
});
}
catch (error) {
if (error.code !== 'ENOENT') {
this.logger.error('Failed to load connection state', error);
}
}
}
async loadMetrics() {
try {
const data = await fs.readFile(this.metricsPath, 'utf-8');
const loaded = JSON.parse(data);
// Convert date strings back to Date objects
loaded.connectionHistory = loaded.connectionHistory.map((event) => ({
...event,
timestamp: new Date(event.timestamp),
}));
this.metrics = loaded;
this.connectionHistory = loaded.connectionHistory;
this.logger.info('Connection metrics loaded', {
totalConnections: this.metrics.totalConnections,
historySize: this.connectionHistory.length,
});
}
catch (error) {
if (error.code !== 'ENOENT') {
this.logger.error('Failed to load connection metrics', error);
}
}
}
async persistState() {
if (!this.config.enablePersistence) {
return;
}
try {
if (this.currentState) {
await fs.writeFile(this.statePath, JSON.stringify(this.currentState, null, 2), 'utf-8');
}
// Also persist metrics
await fs.writeFile(this.metricsPath, JSON.stringify({
...this.metrics,
connectionHistory: this.connectionHistory,
}, null, 2), 'utf-8');
this.logger.debug('State and metrics persisted');
}
catch (error) {
this.logger.error('Failed to persist state', error);
}
}
startPersistenceTimer() {
if (this.persistenceTimer) {
return;
}
this.persistenceTimer = setInterval(() => {
this.persistState().catch((error) => {
this.logger.error('Periodic persistence failed', error);
});
}, this.config.persistenceInterval);
}
/**
* Cleanup resources
*/
async cleanup() {
if (this.persistenceTimer) {
clearInterval(this.persistenceTimer);
this.persistenceTimer = undefined;
}
// Final persistence
await this.persistState();
}
}
//# sourceMappingURL=connection-state-manager.js.map