import { io, Socket } from 'socket.io-client'; import { EventEmitter } from 'events'; import type { ChatMessage, SessionRequest, UserMessage } from './types.js'; export class ChatSession extends EventEmitter { private socket: Socket | null = null; private sessionId: string | null = null; private userId: string; private botType: string; private middlewareUrl: string; private connected = false; private streamBuffers: Map = new Map(); constructor(botType: string, middlewareUrl: string, sessionId?: string) { super(); this.botType = botType; this.middlewareUrl = middlewareUrl; this.userId = `cli-user-${Date.now()}`; this.sessionId = sessionId || null; } async connect(): Promise { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Connection timed out after 10s')); this.disconnect(); }, 10000); this.socket = io(this.middlewareUrl, { transports: ['websocket'], reconnection: true, reconnectionAttempts: 3, }); // Handle stream_bot_uttered events before session_confirm (greeting streams first) this.socket.on('stream_bot_uttered', (data: ChatMessage) => { this.handleStreamMessage(data); }); this.socket.on('connect', () => { const request: SessionRequest = { session_id: this.sessionId, bot_type: this.botType, user_id: this.userId, customData: {}, }; this.socket!.emit('session_request', request); }); this.socket.on('session_confirm', (data: { session_id: string }) => { clearTimeout(timeout); this.sessionId = data.session_id; this.connected = true; this.emit('connected', this.sessionId); resolve(this.sessionId); }); this.socket.on('bot_uttered', (data: ChatMessage) => { if (data.text) { this.emit('message', data.text); } }); this.socket.on('connect_error', (err: Error) => { clearTimeout(timeout); reject(new Error(`Connection failed: ${err.message}`)); }); this.socket.on('disconnect', (reason: string) => { this.connected = false; this.emit('disconnected', reason); }); }); } private handleStreamMessage(data: ChatMessage): void { if (!data.streamId) return; const streamId = data.streamId; if (data.isFirstChunk) { this.streamBuffers.set(streamId, ''); } if (data.text) { const current = this.streamBuffers.get(streamId) || ''; this.streamBuffers.set(streamId, current + data.text); } this.emit('stream', data); if (data.streamFinished) { const fullText = this.streamBuffers.get(streamId) || ''; this.streamBuffers.delete(streamId); if (fullText) { this.emit('message', fullText); } } } send(message: string): void { if (!this.socket || !this.connected || !this.sessionId) { throw new Error('Not connected. Call connect() first.'); } const payload: UserMessage = { message, messageType: 'text', bot_type: this.botType, user_id: this.userId, session_id: this.sessionId, is_user_message_sent: true, }; this.socket.emit('user_uttered', payload); } disconnect(): void { if (this.socket) { if (this.sessionId) { this.socket.emit('session_closed', { session_id: this.sessionId }); } this.socket.disconnect(); this.socket = null; } this.connected = false; this.sessionId = null; } getSessionId(): string | null { return this.sessionId; } }