'use strict'; const net = require('net'); const { EventEmitter } = require('events'); function encodeHex(value) { return Buffer.from(String(value ?? ''), 'utf8').toString('hex') || '-'; } function decodeHex(value) { if (!value || value === '-') return ''; return Buffer.from(value, 'hex').toString('utf8'); } class RuntimeConnection extends EventEmitter { constructor() { super(); this.socket = null; this.buffer = ''; this.nextRequestId = 1; this.pending = new Map(); this.closed = false; } connect(host, port) { return new Promise((resolve, reject) => { const socket = net.createConnection({ host, port }); const onError = (error) => { socket.destroy(); reject(error); }; socket.once('error', onError); socket.once('connect', () => { socket.removeListener('error', onError); this.socket = socket; this.closed = false; socket.setEncoding('utf8'); socket.on('data', (chunk) => this.onData(chunk)); socket.on('error', (error) => this.emit('runtimeError', error)); socket.on('close', () => this.onClose()); resolve(); }); }); } request(command, fields = [], timeoutMs = 30000) { if (!this.socket || this.closed) { return Promise.reject(new Error('NX runtime is not connected')); } const id = this.nextRequestId++; const normalized = fields.map((field) => String(field ?? '')); if (normalized.some((field) => field.includes('\t') || field.includes('\n'))) { return Promise.reject(new Error('protocol fields must be hex-encoded before transmission')); } return new Promise((resolve, reject) => { const timer = setTimeout(() => { this.pending.delete(id); reject(new Error(`NX ${command} request timed out`)); }, timeoutMs); this.pending.set(id, { resolve, reject, timer, command }); this.socket.write(['REQ', id, command, ...normalized].join('\t') + '\n'); }); } close() { this.closed = true; if (this.socket) this.socket.destroy(); this.socket = null; } onData(chunk) { this.buffer += chunk; let newline; while ((newline = this.buffer.indexOf('\n')) >= 0) { const line = this.buffer.slice(0, newline).replace(/\r$/, ''); this.buffer = this.buffer.slice(newline + 1); if (line) this.onLine(line); } } onLine(line) { const fields = line.split('\t'); if (fields[0] === 'RES') { const id = Number(fields[1]); const pending = this.pending.get(id); if (!pending) return; this.pending.delete(id); clearTimeout(pending.timer); if (fields[2] === 'OK') { pending.resolve({ command: fields[3], fields: fields.slice(4) }); } else { pending.reject(new Error(decodeHex(fields[4]) || `NX ${pending.command} failed`)); } return; } if (fields[0] === 'EVENT') { this.emit('event', fields[1], fields.slice(2)); return; } this.emit('runtimeError', new Error(`unrecognized NX message: ${line}`)); } onClose() { if (this.closed) return; this.closed = true; for (const pending of this.pending.values()) { clearTimeout(pending.timer); pending.reject(new Error('NX runtime disconnected')); } this.pending.clear(); this.emit('close'); } } module.exports = { RuntimeConnection, encodeHex, decodeHex };