summaryrefslogtreecommitdiffhomepage
path: root/packages/lsp/src/rpc.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/lsp/src/rpc.ts')
-rw-r--r--packages/lsp/src/rpc.ts288
1 files changed, 144 insertions, 144 deletions
diff --git a/packages/lsp/src/rpc.ts b/packages/lsp/src/rpc.ts
index 95157de..442176a 100644
--- a/packages/lsp/src/rpc.ts
+++ b/packages/lsp/src/rpc.ts
@@ -10,157 +10,157 @@ import { encode } from "./framing.js";
export type WriteFn = (bytes: Uint8Array) => void;
export interface PendingRequest {
- readonly resolve: (value: unknown) => void;
- readonly reject: (reason: unknown) => void;
+ readonly resolve: (value: unknown) => void;
+ readonly reject: (reason: unknown) => void;
}
export type RequestHandler = (params: unknown) => unknown | Promise<unknown>;
export type NotificationHandler = (params: unknown) => void;
export interface JsonRpcMessage {
- readonly jsonrpc: "2.0";
- readonly id?: number | string | undefined;
- readonly method?: string | undefined;
- readonly params?: unknown;
- readonly result?: unknown;
- readonly error?:
- | { readonly code: number; readonly message: string; readonly data?: unknown }
- | undefined;
+ readonly jsonrpc: "2.0";
+ readonly id?: number | string | undefined;
+ readonly method?: string | undefined;
+ readonly params?: unknown;
+ readonly result?: unknown;
+ readonly error?:
+ | { readonly code: number; readonly message: string; readonly data?: unknown }
+ | undefined;
}
export class JsonRpcConnection {
- private nextId = 1;
- private pending = new Map<number | string, PendingRequest>();
- private requestHandlers = new Map<string, RequestHandler>();
- private notificationHandlers = new Map<string, NotificationHandler>();
- private write: WriteFn;
-
- constructor(write: WriteFn) {
- this.write = write;
- }
-
- /**
- * Send a request and await the correlated response. If `timeoutMs` is given,
- * the promise rejects with a timeout error after that long — so a dead/slow
- * server can't hang the caller forever (hover/definition/references).
- * No `timeoutMs` = wait indefinitely (used by the initialize handshake, which
- * has its own 45s race).
- */
- sendRequest(method: string, params?: unknown, timeoutMs?: number): Promise<unknown> {
- const id = this.nextId++;
- const msg: JsonRpcMessage = { jsonrpc: "2.0", id, method, params };
- return new Promise((resolve, reject) => {
- let timer: ReturnType<typeof setTimeout> | undefined;
- // Wrap resolve/reject so the timer is cleared on a normal response
- // (or on dispose) — no dangling timer after completion.
- const finish = (fn: () => void): void => {
- if (timer) clearTimeout(timer);
- fn();
- };
- const entry: PendingRequest = {
- resolve: (value: unknown) => finish(() => resolve(value)),
- reject: (reason: unknown) => finish(() => reject(reason)),
- };
- if (timeoutMs !== undefined) {
- timer = setTimeout(() => {
- if (this.pending.delete(id)) {
- reject(new Error(`LSP request timed out after ${timeoutMs}ms: ${method}`));
- }
- }, timeoutMs);
- }
- this.pending.set(id, entry);
- this.sendMessage(msg);
- });
- }
-
- sendNotification(method: string, params?: unknown): void {
- const msg: JsonRpcMessage = { jsonrpc: "2.0", method, params };
- this.sendMessage(msg);
- }
-
- onRequest(method: string, handler: RequestHandler): void {
- this.requestHandlers.set(method, handler);
- }
-
- onNotification(method: string, handler: NotificationHandler): void {
- this.notificationHandlers.set(method, handler);
- }
-
- async handleMessage(json: string): Promise<void> {
- let msg: JsonRpcMessage;
- try {
- msg = JSON.parse(json) as JsonRpcMessage;
- } catch {
- // A malformed LSP message must never crash the server. The most
- // common cause is a multi-byte UTF-8 character split across stdout
- // chunks (see FrameDecoder). Log and skip — the language server
- // will re-send diagnostics on the next file change.
- return;
- }
- const { id, method } = msg;
-
- if (id !== undefined && method !== undefined) {
- await this.handleIncomingRequest(id, method, msg.params);
- } else if (method !== undefined) {
- this.handleIncomingNotification(method, msg.params);
- } else if (id !== undefined) {
- this.handleResponse(id, msg);
- }
- }
-
- private sendMessage(msg: JsonRpcMessage): void {
- this.write(encode(JSON.stringify(msg)));
- }
-
- private handleResponse(id: number | string, msg: JsonRpcMessage): void {
- const entry = this.pending.get(id);
- if (!entry) return;
- this.pending.delete(id);
- if (msg.error) {
- entry.reject(new Error(msg.error.message));
- } else {
- entry.resolve(msg.result);
- }
- }
-
- private async handleIncomingRequest(
- id: number | string,
- method: string,
- params: unknown,
- ): Promise<void> {
- const handler = this.requestHandlers.get(method);
- if (!handler) {
- this.sendMessage({
- jsonrpc: "2.0",
- id,
- error: { code: -32601, message: `Method not found: ${method}` },
- });
- return;
- }
- try {
- const result = await handler(params);
- this.sendMessage({ jsonrpc: "2.0", id, result });
- } catch (err: unknown) {
- const message = err instanceof Error ? err.message : String(err);
- this.sendMessage({
- jsonrpc: "2.0",
- id,
- error: { code: -32603, message },
- });
- }
- }
-
- private handleIncomingNotification(method: string, params: unknown): void {
- const handler = this.notificationHandlers.get(method);
- if (handler) {
- handler(params);
- }
- }
-
- dispose(): void {
- for (const entry of this.pending.values()) {
- entry.reject(new Error("Connection closed"));
- }
- this.pending.clear();
- }
+ private nextId = 1;
+ private pending = new Map<number | string, PendingRequest>();
+ private requestHandlers = new Map<string, RequestHandler>();
+ private notificationHandlers = new Map<string, NotificationHandler>();
+ private write: WriteFn;
+
+ constructor(write: WriteFn) {
+ this.write = write;
+ }
+
+ /**
+ * Send a request and await the correlated response. If `timeoutMs` is given,
+ * the promise rejects with a timeout error after that long — so a dead/slow
+ * server can't hang the caller forever (hover/definition/references).
+ * No `timeoutMs` = wait indefinitely (used by the initialize handshake, which
+ * has its own 45s race).
+ */
+ sendRequest(method: string, params?: unknown, timeoutMs?: number): Promise<unknown> {
+ const id = this.nextId++;
+ const msg: JsonRpcMessage = { jsonrpc: "2.0", id, method, params };
+ return new Promise((resolve, reject) => {
+ let timer: ReturnType<typeof setTimeout> | undefined;
+ // Wrap resolve/reject so the timer is cleared on a normal response
+ // (or on dispose) — no dangling timer after completion.
+ const finish = (fn: () => void): void => {
+ if (timer) clearTimeout(timer);
+ fn();
+ };
+ const entry: PendingRequest = {
+ resolve: (value: unknown) => finish(() => resolve(value)),
+ reject: (reason: unknown) => finish(() => reject(reason)),
+ };
+ if (timeoutMs !== undefined) {
+ timer = setTimeout(() => {
+ if (this.pending.delete(id)) {
+ reject(new Error(`LSP request timed out after ${timeoutMs}ms: ${method}`));
+ }
+ }, timeoutMs);
+ }
+ this.pending.set(id, entry);
+ this.sendMessage(msg);
+ });
+ }
+
+ sendNotification(method: string, params?: unknown): void {
+ const msg: JsonRpcMessage = { jsonrpc: "2.0", method, params };
+ this.sendMessage(msg);
+ }
+
+ onRequest(method: string, handler: RequestHandler): void {
+ this.requestHandlers.set(method, handler);
+ }
+
+ onNotification(method: string, handler: NotificationHandler): void {
+ this.notificationHandlers.set(method, handler);
+ }
+
+ async handleMessage(json: string): Promise<void> {
+ let msg: JsonRpcMessage;
+ try {
+ msg = JSON.parse(json) as JsonRpcMessage;
+ } catch {
+ // A malformed LSP message must never crash the server. The most
+ // common cause is a multi-byte UTF-8 character split across stdout
+ // chunks (see FrameDecoder). Log and skip — the language server
+ // will re-send diagnostics on the next file change.
+ return;
+ }
+ const { id, method } = msg;
+
+ if (id !== undefined && method !== undefined) {
+ await this.handleIncomingRequest(id, method, msg.params);
+ } else if (method !== undefined) {
+ this.handleIncomingNotification(method, msg.params);
+ } else if (id !== undefined) {
+ this.handleResponse(id, msg);
+ }
+ }
+
+ private sendMessage(msg: JsonRpcMessage): void {
+ this.write(encode(JSON.stringify(msg)));
+ }
+
+ private handleResponse(id: number | string, msg: JsonRpcMessage): void {
+ const entry = this.pending.get(id);
+ if (!entry) return;
+ this.pending.delete(id);
+ if (msg.error) {
+ entry.reject(new Error(msg.error.message));
+ } else {
+ entry.resolve(msg.result);
+ }
+ }
+
+ private async handleIncomingRequest(
+ id: number | string,
+ method: string,
+ params: unknown,
+ ): Promise<void> {
+ const handler = this.requestHandlers.get(method);
+ if (!handler) {
+ this.sendMessage({
+ jsonrpc: "2.0",
+ id,
+ error: { code: -32601, message: `Method not found: ${method}` },
+ });
+ return;
+ }
+ try {
+ const result = await handler(params);
+ this.sendMessage({ jsonrpc: "2.0", id, result });
+ } catch (err: unknown) {
+ const message = err instanceof Error ? err.message : String(err);
+ this.sendMessage({
+ jsonrpc: "2.0",
+ id,
+ error: { code: -32603, message },
+ });
+ }
+ }
+
+ private handleIncomingNotification(method: string, params: unknown): void {
+ const handler = this.notificationHandlers.get(method);
+ if (handler) {
+ handler(params);
+ }
+ }
+
+ dispose(): void {
+ for (const entry of this.pending.values()) {
+ entry.reject(new Error("Connection closed"));
+ }
+ this.pending.clear();
+ }
}