import readline from 'node:readline'; // 受信順に依存せず、応答・通知・サーバー要求を振り分ける。 export class RpcClient { constructor(proc, { onNotification = () => {}, onServerRequest, timeoutMs = 15000 } = {}) { this.proc = proc; this.pending = new Map(); this.nextId = 1; this.timeoutMs = timeoutMs; this.closed = false; this.rl = readline.createInterface({ input: proc.stdout }); this.rl.on('line', line => { try { const m = JSON.parse(line); const hasId = Object.hasOwn(m, 'id'); if (m.method && hasId) { // 両方向の id は別物。同じ値でも pending の応答として扱わない。 Promise.resolve().then(() => onServerRequest?.(m)).then(result => { this.send(result === undefined ? { id: m.id, error: { code: -32601, message: 'Unsupported server request' } } : { id: m.id, result }); }).catch(() => { if (!this.closed) this.send({ id: m.id, error: { code: -32603, message: 'Client handler failed' } }); }); } else if (m.method) { onNotification(m); } else if (hasId) { const p = this.pending.get(m.id); if (!p) return; clearTimeout(p.timer); this.pending.delete(m.id); m.error ? p.reject(new Error(`${m.error.code}: ${m.error.message}`)) : p.resolve(m.result); } } catch (error) { this.fail(error); } }); proc.on('error', error => this.fail(error)); proc.on('exit', (code, signal) => this.fail(new Error(`server exit: ${code ?? signal}`))); proc.stdin.on('error', error => this.fail(error)); proc.stdout.on('error', error => this.fail(error)); } send(message) { if (this.closed) throw new Error('connection closed'); this.proc.stdin.write(JSON.stringify(message) + '\n'); } request(method, params = {}) { if (this.closed) return Promise.reject(new Error('connection closed')); const id = this.nextId++; return new Promise((resolve, reject) => { const timer = setTimeout(() => { this.pending.delete(id); reject(new Error(`timeout: ${method}; 結果不明のため自動再送しません`)); }, this.timeoutMs); this.pending.set(id, { resolve, reject, timer }); try { this.send({ id, method, params }); } catch (error) { clearTimeout(timer); this.pending.delete(id); reject(error); } }); } notify(method, params = {}) { this.send({ method, params }); } fail(error) { this.closed = true; for (const p of this.pending.values()) { clearTimeout(p.timer); p.reject(error); } this.pending.clear(); } close() { this.fail(new Error('client closed')); this.rl.close(); this.proc.stdin.end(); this.proc.kill(); } }