{"version":3,"file":"jsonrpc.d.ts","sourceRoot":"","sources":["../../../src/core/lsp/jsonrpc.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,oBAAoB,CAAC;AACzE,OAAO,KAAK,EAAE,sBAAsB,EAAyC,MAAM,YAAY,CAAC;AAehG,MAAM,WAAW,cAAc;IAC9B,KAAK,EAAE,8BAA8B,CAAC;IACtC,cAAc,CAAC,EAAE,CAAC,YAAY,EAAE,sBAAsB,KAAK,IAAI,CAAC;IAChE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,qBAAa,aAAa;IACzB,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,OAAO,CAAoG;IACnH,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,KAAK,CAAiC;IAC9C,OAAO,CAAC,cAAc,CAAC,CAAsC;IAC7D,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAS;IAEzB,YAAY,IAAI,EAAE,cAAc,EAM/B;IAED,OAAO,CAAC,UAAU,CAA2B;IAE7C,2EAA2E;IAC3E,sBAAsB,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,sBAAsB,KAAK,IAAI,CAAC,GAAG,SAAS,GAAG,IAAI,CAEvF;IAED,OAAO,CAAC,aAAa;IAIrB,mBAAmB,IAAI,MAAM,CAE5B;IAED,2EAA2E;IAC3E,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CA0BtF;IAED,kDAAkD;IAClD,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,CAI5C;IAED,+DAA+D;IAC/D,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAE9B;IAED,4DAA4D;IAC5D,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAGrC;IAED,OAAO,CAAC,IAAI;IAMZ,OAAO,CAAC,MAAM;IAgBd,OAAO,CAAC,QAAQ;IAiBhB,OAAO,CAAC,QAAQ;IA4BhB,OAAO,IAAI,IAAI,CAGd;CACD","sourcesContent":["import type { ChildProcessWithoutNullStreams } from \"node:child_process\";\nimport type { LspNotificationMessage, LspRequestMessage, LspResponseMessage } from \"./types.js\";\n\n/**\n * Minimal JSON-RPC 2.0 transport over a stdio child process, with\n * Content-Length framing and request cancellation. Server output is treated as\n * untrusted data and is never interpreted as instructions.\n *\n * Message-limit guard: a pathological server must not be allowed to buffer\n * unbounded frames in memory.\n */\n\nconst MAX_FRAME_BYTES = 16 * 1024 * 1024; // 16 MiB per message\nconst MAX_PENDING_REQUESTS = 128;\nconst DEFAULT_REQUEST_TIMEOUT_MS = 30_000;\n\nexport interface JsonRpcOptions {\n\tchild: ChildProcessWithoutNullStreams;\n\tonNotification?: (notification: LspNotificationMessage) => void;\n\trequestTimeoutMs?: number;\n}\n\nexport class JsonRpcClient {\n\tprivate nextId = 1;\n\tprivate pending = new Map<number, { resolve: (r: unknown) => void; reject: (e: Error) => void; method: string }>();\n\tprivate buffer: Buffer = Buffer.alloc(0);\n\tprivate child: ChildProcessWithoutNullStreams;\n\tprivate onNotification?: (n: LspNotificationMessage) => void;\n\tprivate requestTimeoutMs: number;\n\tprivate disposed = false;\n\n\tconstructor(opts: JsonRpcOptions) {\n\t\tthis.child = opts.child;\n\t\tthis.onNotification = opts.onNotification;\n\t\tthis.requestTimeoutMs = opts.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n\t\tthis.child.stdout.on(\"data\", (d: Buffer) => this.onData(d));\n\t\tthis.child.stderr.on(\"data\", (d: Buffer) => this.captureStderr(d));\n\t}\n\n\tprivate stderrTail: Buffer = Buffer.alloc(0);\n\n\t/** Set/replace the notification handler (e.g. LSP diagnostics routing). */\n\tsetNotificationHandler(handler: ((n: LspNotificationMessage) => void) | undefined): void {\n\t\tthis.onNotification = handler;\n\t}\n\n\tprivate captureStderr(d: Buffer): void {\n\t\tthis.stderrTail = Buffer.concat([this.stderrTail, d]).subarray(-16384);\n\t}\n\n\tgetDiagnosticStderr(): string {\n\t\treturn this.stderrTail.toString(\"utf-8\");\n\t}\n\n\t/** Send a request. Returns a promise resolved/rejected by the response. */\n\trequest<TResult>(method: string, params: unknown, timeoutMs?: number): Promise<TResult> {\n\t\tif (this.disposed) return Promise.reject(new Error(\"jsonrpc transport disposed\"));\n\t\tif (this.pending.size >= MAX_PENDING_REQUESTS) {\n\t\t\treturn Promise.reject(new Error(`too many pending LSP requests (${this.pending.size})`));\n\t\t}\n\t\tconst id = this.nextId++;\n\t\tconst msg: LspRequestMessage = { jsonrpc: \"2.0\", id, method, params };\n\t\tconst timeout = timeoutMs ?? this.requestTimeoutMs;\n\t\treturn new Promise<TResult>((resolve, reject) => {\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tthis.pending.delete(id);\n\t\t\t\treject(new Error(`LSP request ${method} (#${id}) timed out after ${timeout}ms`));\n\t\t\t}, timeout);\n\t\t\tthis.pending.set(id, {\n\t\t\t\tresolve: (r) => {\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\tresolve(r as TResult);\n\t\t\t\t},\n\t\t\t\treject: (e) => {\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\treject(e);\n\t\t\t\t},\n\t\t\t\tmethod,\n\t\t\t});\n\t\t\tthis.send(msg);\n\t\t});\n\t}\n\n\t/** Send a notification (no response expected). */\n\tnotify(method: string, params: unknown): void {\n\t\tif (this.disposed) return;\n\t\tconst msg: LspNotificationMessage = { jsonrpc: \"2.0\", method, params };\n\t\tthis.send(msg);\n\t}\n\n\t/** Send a server-cancellation $/cancelRequest notification. */\n\tcancelRequest(id: number): void {\n\t\tthis.notify(\"$/cancelRequest\", { id });\n\t}\n\n\t/** Reject all pending requests (used on shutdown/crash). */\n\trejectAllPending(reason: string): void {\n\t\tfor (const [, p] of this.pending) p.reject(new Error(reason));\n\t\tthis.pending.clear();\n\t}\n\n\tprivate send(msg: unknown): void {\n\t\tconst body = Buffer.from(JSON.stringify(msg), \"utf-8\");\n\t\tconst header = Buffer.from(`Content-Length: ${body.length}\\r\\n\\r\\n`, \"ascii\");\n\t\tthis.child.stdin.write(Buffer.concat([header, body]));\n\t}\n\n\tprivate onData(chunk: Buffer): void {\n\t\tthis.buffer = Buffer.concat([this.buffer, chunk]);\n\t\tif (this.buffer.length > MAX_FRAME_BYTES * 2) {\n\t\t\t// Bounded buffer: reject pending and reset to avoid OOM.\n\t\t\tthis.rejectAllPending(\"LSP server flooded transport with oversized frames\");\n\t\t\tthis.buffer = Buffer.alloc(0);\n\t\t\treturn;\n\t\t}\n\t\twhile (true) {\n\t\t\tconst { message, consumed } = this.tryParse();\n\t\t\tif (message === null) break;\n\t\t\tthis.buffer = this.buffer.subarray(consumed);\n\t\t\tthis.dispatch(message);\n\t\t}\n\t}\n\n\tprivate tryParse(): { message: unknown | null; consumed: number } {\n\t\tconst headerEnd = this.buffer.indexOf(\"\\r\\n\\r\\n\");\n\t\tif (headerEnd === -1) return { message: null, consumed: 0 };\n\t\tconst headerText = this.buffer.subarray(0, headerEnd).toString(\"ascii\");\n\t\tconst lengthMatch = /Content-Length:\\s*(\\d+)/i.exec(headerText);\n\t\tif (!lengthMatch) return { message: null, consumed: 0 };\n\t\tconst length = Number(lengthMatch[1]);\n\t\tconst total = headerEnd + 4 + length;\n\t\tif (this.buffer.length < total) return { message: null, consumed: 0 };\n\t\tconst body = this.buffer.subarray(headerEnd + 4, total);\n\t\ttry {\n\t\t\treturn { message: JSON.parse(body.toString(\"utf-8\")), consumed: total };\n\t\t} catch {\n\t\t\treturn { message: null, consumed: total };\n\t\t}\n\t}\n\n\tprivate dispatch(message: unknown): void {\n\t\tconst msg = message as Partial<LspRequestMessage & LspResponseMessage & LspNotificationMessage>;\n\t\tif (typeof msg.id === \"number\") {\n\t\t\t// Response\n\t\t\tconst pending = this.pending.get(msg.id);\n\t\t\tif (pending) {\n\t\t\t\tthis.pending.delete(msg.id);\n\t\t\t\tif (msg.error) {\n\t\t\t\t\tconst err = new Error(`LSP error ${msg.error.code}: ${msg.error.message}`);\n\t\t\t\t\t(err as { code?: number }).code = msg.error.code;\n\t\t\t\t\tpending.reject(err);\n\t\t\t\t} else {\n\t\t\t\t\tpending.resolve(msg.result);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (msg.id === null) {\n\t\t\t// Server-to-client request (e.g. workspace/applyEdit). Not supported\n\t\t\t// for mutation; respond nothing.\n\t\t\treturn;\n\t\t}\n\t\t// Notification (method present, no id).\n\t\tif (typeof msg.method === \"string\") {\n\t\t\tthis.onNotification?.(msg as LspNotificationMessage);\n\t\t}\n\t}\n\n\tdispose(): void {\n\t\tthis.disposed = true;\n\t\tthis.rejectAllPending(\"jsonrpc transport disposed\");\n\t}\n}\n"]}