Skip to main content
Glama
initial-history-replay-capture.ts7.34 kB
/** * Story 4: Initial History Replay Capture Implementation * * InitialHistoryReplayCapture captures initial WebSocket messages when browsers connect * to terminal sessions, distinguishing between history replay and real-time messages. * * This class works with WebSocketConnectionDiscovery from Story 3 to capture the initial * terminal history that browsers see when they first load the terminal. * * CRITICAL: No mocks in production code. This uses real WebSocket connections. */ import { WebSocketConnectionDiscovery } from './websocket-connection-discovery'; import WebSocket from 'ws'; /** * Configuration for InitialHistoryReplayCapture */ export interface InitialHistoryReplayCaptureConfig { historyReplayTimeout?: number; // How long to wait for history replay to complete captureTimeout?: number; // How long to capture messages maxHistoryMessages?: number; // Maximum number of history messages to capture } /** * Represents a captured WebSocket message */ export interface CapturedMessage { timestamp: number; data: any; // Must handle both string and object data from WebSocket messages type: 'websocket_sent' | 'websocket_received' | 'history_replay'; isHistoryReplay: boolean; sequenceNumber: number; } /** * InitialHistoryReplayCapture class - captures initial terminal history on WebSocket connection */ export class InitialHistoryReplayCapture { private connectionDiscovery?: WebSocketConnectionDiscovery; private config: Required<InitialHistoryReplayCaptureConfig>; private capturing: boolean = false; private historyMessages: CapturedMessage[] = []; private realTimeMessages: CapturedMessage[] = []; private sequenceNumber: number = 0; private messageHandler?: (data: Buffer) => void; private currentWebSocket?: WebSocket; private historyReplayComplete: boolean = false; private historyReplayPromise?: Promise<void>; private historyReplayTimerRef?: NodeJS.Timeout; constructor( connectionDiscovery?: WebSocketConnectionDiscovery, config: InitialHistoryReplayCaptureConfig = {} ) { this.connectionDiscovery = connectionDiscovery; this.config = { historyReplayTimeout: config.historyReplayTimeout ?? 5000, // Fixed: Must be much shorter than workflow timeout to prevent race condition captureTimeout: config.captureTimeout ?? 60000, maxHistoryMessages: config.maxHistoryMessages ?? 10000 }; } /** * Start capturing initial history replay messages */ async captureInitialHistory(webSocket: WebSocket): Promise<void> { this.capturing = true; this.sequenceNumber = 0; this.currentWebSocket = webSocket; this.historyReplayComplete = false; // Initialize connection discovery if provided if (this.connectionDiscovery) { // Will be used in future implementation } // Create message handler for capturing WebSocket messages this.messageHandler = (data: Buffer) => { this.handleMessage(data.toString()); }; // Attach message listener to WebSocket webSocket.on('message', this.messageHandler); // Start the history replay completion timer this.startHistoryReplayTimer(); } /** * Handle incoming WebSocket message */ private handleMessage(data: string): void { if (!this.capturing) { return; } // Parse JSON WebSocket message and extract terminal data // CRITICAL: Extract only the terminal data field to preserve CRLF formatting let terminalData: string; try { const parsed = JSON.parse(data); // Extract the actual terminal data from the JSON message terminalData = parsed.data || ''; } catch (error) { // If not JSON or no data field, treat as raw message (fallback) terminalData = data; } const capturedMessage: CapturedMessage = { timestamp: Date.now(), data: terminalData, // Store only the terminal data, not the JSON wrapper type: this.historyReplayComplete ? 'websocket_received' : 'history_replay', isHistoryReplay: !this.historyReplayComplete, // History replay if not yet complete sequenceNumber: ++this.sequenceNumber }; // Separate messages based on whether history replay is complete if (this.historyReplayComplete) { this.realTimeMessages.push(capturedMessage); } else { // Check maxHistoryMessages limit if (this.historyMessages.length < this.config.maxHistoryMessages) { this.historyMessages.push(capturedMessage); } // If limit exceeded, silently drop the message (for history replay phase only) } } /** * Start history replay completion timer */ private startHistoryReplayTimer(): void { this.historyReplayPromise = new Promise<void>((resolve) => { this.historyReplayTimerRef = setTimeout(() => { this.historyReplayComplete = true; resolve(); }, this.config.historyReplayTimeout); }); } /** * Wait for history replay to complete */ async waitForHistoryReplayComplete(): Promise<void> { if (this.historyReplayPromise) { await this.historyReplayPromise; } } /** * Distinguish between history replay and real-time messages */ distinguishMessageTypes(message: CapturedMessage): boolean { // Minimal implementation - will be expanded with TDD return message.isHistoryReplay; } /** * Get captured history messages */ getHistoryMessages(): CapturedMessage[] { return [...this.historyMessages]; } /** * Get captured real-time messages */ getRealTimeMessages(): CapturedMessage[] { return [...this.realTimeMessages]; } /** * Check if currently capturing messages */ isCapturing(): boolean { return this.capturing; } /** * Get current configuration */ getConfig(): Required<InitialHistoryReplayCaptureConfig> { return { ...this.config }; } /** * Get current sequence number */ getSequenceNumber(): number { return this.sequenceNumber; } /** * Capture WebSocket sent message (called by PostWebSocketCommandExecutor) */ captureWebSocketMessage(type: 'websocket_sent' | 'websocket_received', data: any): void { if (!this.capturing) { return; } const capturedMessage: CapturedMessage = { timestamp: Date.now(), data: data, type: type, isHistoryReplay: false, // Sent messages are never history replay sequenceNumber: ++this.sequenceNumber }; // Always add to real-time messages since sent messages are always real-time this.realTimeMessages.push(capturedMessage); } /** * Cleanup resources */ async cleanup(): Promise<void> { this.capturing = false; // Remove event listeners if (this.currentWebSocket && this.messageHandler) { this.currentWebSocket.off('message', this.messageHandler); } // Cancel any pending timer if (this.historyReplayTimerRef) { clearTimeout(this.historyReplayTimerRef); this.historyReplayTimerRef = undefined; } // Clear data this.historyMessages = []; this.realTimeMessages = []; this.sequenceNumber = 0; this.messageHandler = undefined; this.currentWebSocket = undefined; this.historyReplayComplete = false; this.historyReplayPromise = undefined; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/LightspeedDMS/ssh-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server