import { EventEmitter } from 'events'; import { Socket } from 'net'; import { encodeTPKT, parseTPKTHeader, isValidTPKTHeader } from '../protocol/tpkt.js'; import { TPKT_HEADER_LEN } from '../protocol/constants.js'; const DEFAULT_RECV_TIMEOUT = 5000; export class TcpTransport extends EventEmitter { private socket: Socket | null = null; private recvBuffer = Buffer.alloc(0); private pendingResolve: ((data: Buffer) => void) | null = null; private pendingReject: ((err: Error) => void) | null = null; private recvTimer: ReturnType | null = null; private _recvTimeout = DEFAULT_RECV_TIMEOUT; async connect(host: string, port: number, timeout = 5000): Promise { // Disconnect old transport before creating new connection this.disconnect(); return new Promise((resolve, reject) => { const socket = new Socket(); socket.setTimeout(timeout); const onError = (err: Error) => { cleanup(); socket.destroy(); reject(err); }; const onConnect = () => { cleanup(); this.socket = socket; socket.setTimeout(0); socket.on('data', (data: Buffer) => this.onData(data)); socket.on('close', () => this.onClose()); socket.on('error', () => {}); // swallow errors after connect resolve(); }; const onTimeout = () => { cleanup(); socket.destroy(); reject(new Error('Connection timeout')); }; const cleanup = () => { socket.removeListener('error', onError); socket.removeListener('connect', onConnect); socket.removeListener('timeout', onTimeout); }; socket.once('error', onError); socket.once('connect', onConnect); socket.once('timeout', onTimeout); socket.connect(port, host); }); } disconnect(): void { this.clearRecvTimer(); this.rejectPending(new Error('Transport disconnected')); if (this.socket) { this.socket.removeAllListeners(); this.socket.destroy(); this.socket = null; } this.recvBuffer = Buffer.alloc(0); } isConnected(): boolean { return this.socket !== null && !this.socket.destroyed; } setRecvTimeout(ms: number): void { this._recvTimeout = ms; } setSendTimeout(ms: number): void { if (this.socket) this.socket.setTimeout(ms); } setKeepAlive(enable: boolean, initialDelay = 0): void { if (this.socket) this.socket.setKeepAlive(enable, initialDelay); } async send(cotpPdu: Buffer): Promise { if (!this.socket) throw new Error('Not connected'); const tpktFrame = encodeTPKT(cotpPdu); return new Promise((resolve, reject) => { this.socket!.write(tpktFrame, (err) => { if (err) reject(err); else resolve(); }); }); } async recv(): Promise { const cached = this.tryExtractFrame(); if (cached) return cached; this.clearRecvTimer(); return new Promise((resolve, reject) => { this.pendingResolve = resolve; this.pendingReject = reject; this.recvTimer = setTimeout(() => { this.pendingResolve = null; this.pendingReject = null; this.recvTimer = null; reject(new Error('Recv timeout')); }, this._recvTimeout); }); } private onData(data: Buffer): void { this.recvBuffer = Buffer.concat([this.recvBuffer, data]); if (this.pendingResolve) { const frame = this.tryExtractFrame(); if (frame) { this.clearRecvTimer(); const resolve = this.pendingResolve; this.pendingResolve = null; this.pendingReject = null; resolve(frame); } } } private onClose(): void { this.rejectPending(new Error('Connection reset by peer')); this.socket = null; this.emit('close'); } private rejectPending(err: Error): void { this.clearRecvTimer(); if (this.pendingReject) { const reject = this.pendingReject; this.pendingResolve = null; this.pendingReject = null; reject(err); } else { this.pendingResolve = null; this.pendingReject = null; } } private clearRecvTimer(): void { if (this.recvTimer) { clearTimeout(this.recvTimer); this.recvTimer = null; } } private tryExtractFrame(): Buffer | null { if (this.recvBuffer.length < TPKT_HEADER_LEN) return null; if (!isValidTPKTHeader(this.recvBuffer)) { this.recvBuffer = Buffer.alloc(0); return null; } const { length } = parseTPKTHeader(this.recvBuffer); if (this.recvBuffer.length < length) return null; const payload = this.recvBuffer.subarray(TPKT_HEADER_LEN, length); this.recvBuffer = this.recvBuffer.subarray(length); return Buffer.from(payload); } }