grix-connector
Version:
Connect local AI coding agents (Claude, Codex, Gemini, Qwen, DeepSeek, Cursor, OpenCode, Pi, OpenHuman, Reasonix) to the Grix scheduling platform. Also serves as an OpenClaw plugin for Grix channel transport.
4 lines (3 loc) • 3.54 kB
JavaScript
import{EventEmitter as w}from"events";import{log as h}from"../log/index.js";class _ extends w{nextId=1;pending=new Map;writeLock=Promise.resolve();closed=!1;_readable;_writable;_onNotify;_onRequest;_bridgeLog;constructor(r,e,t,s,o){super(),this._readable=r,this._writable=e,this._onNotify=t,this._onRequest=s,this._bridgeLog=o??null,this.startReadLoop()}setHandlers(r,e){r&&(this._onNotify=r),e&&(this._onRequest=e)}async call(r,e,t){if(this.closed)throw new Error("transport closed");const s=this.nextId++,o=String(s),a=await new Promise((n,c)=>{if(this.pending.set(o,{resolve:n}),t){const i=()=>{this.pending.delete(o),c(new Error(`aborted: ${t.reason??"request cancelled"}`))};if(t.aborted){i();return}t.addEventListener("abort",i,{once:!0});const d=n,f=m=>{t.removeEventListener("abort",i),d(m)};this.pending.set(o,{resolve:f})}this.write({jsonrpc:"2.0",id:s,method:r,params:e??null}).catch(i=>{this.pending.delete(o),c(i)})});if(a.error){const n=new Error(a.error.message);throw n.code=a.error.code,n.data=a.error.data,n}return a.result}async notify(r,e){if(this.closed)throw new Error("transport closed");await this.write({jsonrpc:"2.0",method:r,params:e??null})}async respondSuccess(r,e){await this.write({jsonrpc:"2.0",id:r,result:e})}async respondError(r,e,t){await this.write({jsonrpc:"2.0",id:r,error:{code:e,message:t}})}close(){if(!this.closed){this.closed=!0,this._bridgeLog?.logLifecycle("transport closed");for(const[r,{resolve:e}]of this.pending)e({error:{code:-32e3,message:"transport closed"}});this.pending.clear(),this._readable.destroy(),this.emit("close")}}startReadLoop(){let r="";this._readable.setEncoding("utf8"),this._readable.on("data",e=>{r+=e;const t=r.split(`
`);r=t.pop()??"";for(const s of t){const o=s.trim();o.length!==0&&this.dispatch(o)}}),this._readable.on("end",()=>{r.trim()&&this.dispatch(r.trim()),this.close()}),this._readable.on("error",e=>{this.emitTransportError(e),this.close()})}dispatch(r){let e;try{e=JSON.parse(r)}catch{return}if(this._bridgeLog?.logInbound(u(e),e.id??null,p(e)),e.id!=null&&!e.method){this.completePending(e.id,e.result,e.error);return}if(e.method&&(e.id==null||e.id===null)){this._onNotify?.(e.method,e.params);return}e.method&&e.id!=null&&this._onRequest?.(e.method,e.id,e.params)}completePending(r,e,t){const s=String(r),o=this.pending.get(s);o&&(this.pending.delete(s),o.resolve({result:e,error:t}))}async write(r){if(this.closed)throw new Error("transport closed");this._bridgeLog?.logOutbound(u(r),r?.id??null,p(r));const e=this.writeLock.then(()=>new Promise((t,s)=>{const o=JSON.stringify(r)+`
`;if(this._writable.write(o,"utf-8"))t();else{const n=()=>{i(),t()},c=d=>{i(),s(d)},i=()=>{this._writable.removeListener("drain",n),this._writable.removeListener("error",c)};this._writable.once("drain",n),this._writable.once("error",c)}}));return this.writeLock=e.catch(t=>{h.error("json-rpc",`Write failed: ${t instanceof Error?t.message:t}`)}),e}emitTransportError(r){if(this.listenerCount("error")===0){h.warn("json-rpc",`Transport error (no listeners): ${r.message}`);return}this.emit("error",r)}}function u(l){const r=l;return r&&typeof r.method=="string"?r.method:r&&r.error!=null?"error":r&&"result"in r?"result":"unknown"}function p(l){const r=l;if(!r)return"";if(r.error!=null){const t=r.error;return`error code=${t.code??""} msg=${String(t.message??"").slice(0,120)}`}if("result"in r)return"result";const e=r.params;if(e&&typeof e=="object"){const t=Object.keys(e);return t.length?`params={${t.join(",")}}`:"params={}"}return""}export{_ as JsonRpcTransport};