jay-code
Version:
Streamlined AI CLI orchestration engine with mathematical rigor and enterprise-grade reliability
438 lines (367 loc) • 11.4 kB
text/typescript
/**
* Connection State Manager for MCP
* Persists connection state across disconnections
*/
import { promises as fs } from 'node:fs';
import { join } from 'node:path';
import type { ILogger } from '../../core/logger.js';
import type { MCPRequest, MCPConfig } from '../../utils/types.js';
export interface ConnectionState {
sessionId: string;
lastConnected: Date;
lastDisconnected?: Date;
pendingRequests: MCPRequest[];
configuration: MCPConfig;
metadata: Record<string, unknown>;
}
export interface ConnectionEvent {
timestamp: Date;
type: 'connect' | 'disconnect' | 'reconnect' | 'error';
sessionId: string;
details?: Record<string, unknown>;
error?: string;
}
export interface ConnectionMetrics {
totalConnections: number;
totalDisconnections: number;
totalReconnections: number;
averageSessionDuration: number;
averageReconnectionTime: number;
lastConnectionDuration?: number;
connectionHistory: ConnectionEvent[];
}
export interface StateManagerConfig {
enablePersistence: boolean;
stateDirectory: string;
maxHistorySize: number;
persistenceInterval: number;
}
export class ConnectionStateManager {
private currentState?: ConnectionState;
private connectionHistory: ConnectionEvent[] = [];
private metrics: ConnectionMetrics = {
totalConnections: 0,
totalDisconnections: 0,
totalReconnections: 0,
averageSessionDuration: 0,
averageReconnectionTime: 0,
connectionHistory: [],
};
private persistenceTimer?: NodeJS.Timeout;
private statePath: string;
private metricsPath: string;
private readonly defaultConfig: StateManagerConfig = {
enablePersistence: true,
stateDirectory: '.mcp-state',
maxHistorySize: 1000,
persistenceInterval: 60000, // 1 minute
};
constructor(
private logger: ILogger,
config?: Partial<StateManagerConfig>,
) {
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);
});
}
private config: StateManagerConfig;
/**
* Initialize the state manager
*/
private async initialize(): Promise<void> {
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: ConnectionState): void {
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(): ConnectionState | null {
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: Omit<ConnectionEvent, 'timestamp'>): void {
const fullEvent: ConnectionEvent = {
...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(): ConnectionMetrics {
return {
...this.metrics,
connectionHistory: [...this.connectionHistory],
};
}
/**
* Clear a specific session state
*/
clearSession(sessionId: string): void {
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: MCPRequest): void {
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: string): void {
if (!this.currentState) {
return;
}
this.currentState.pendingRequests = this.currentState.pendingRequests.filter(
(req) => req.id !== requestId,
);
}
/**
* Get pending requests
*/
getPendingRequests(): MCPRequest[] {
return this.currentState?.pendingRequests || [];
}
/**
* Update session metadata
*/
updateMetadata(metadata: Record<string, unknown>): void {
if (!this.currentState) {
return;
}
this.currentState.metadata = {
...this.currentState.metadata,
...metadata,
};
}
/**
* Calculate session duration
*/
getSessionDuration(sessionId: string): number | null {
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: string): number | null {
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();
}
private updateMetrics(event: ConnectionEvent): void {
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;
}
}
private async loadState(): Promise<void> {
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 as any).code !== 'ENOENT') {
this.logger.error('Failed to load connection state', error);
}
}
}
private async loadMetrics(): Promise<void> {
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: any) => ({
...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 as any).code !== 'ENOENT') {
this.logger.error('Failed to load connection metrics', error);
}
}
}
private async persistState(): Promise<void> {
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);
}
}
private startPersistenceTimer(): void {
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(): Promise<void> {
if (this.persistenceTimer) {
clearInterval(this.persistenceTimer);
this.persistenceTimer = undefined;
}
// Final persistence
await this.persistState();
}
}