Path: blob/main/src/vs/base/common/jsonRpcProtocol.ts
13389 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { DeferredPromise } from './async.js';6import { CancellationToken, CancellationTokenSource } from './cancellation.js';7import { CancellationError } from './errors.js';8import { Disposable, toDisposable } from './lifecycle.js';9import { hasKey } from './types.js';1011export type JsonRpcId = string | number;1213export interface IJsonRpcError {14code: number;15message: string;16data?: unknown;17}1819export interface IJsonRpcRequest {20jsonrpc: '2.0';21id: JsonRpcId;22method: string;23params?: unknown;24}2526export interface IJsonRpcNotification {27jsonrpc: '2.0';28method: string;29params?: unknown;30}3132export interface IJsonRpcSuccessResponse {33jsonrpc: '2.0';34id: JsonRpcId;35result: unknown;36}3738export interface IJsonRpcErrorResponse {39jsonrpc: '2.0';40id?: JsonRpcId;41error: IJsonRpcError;42}4344export type JsonRpcMessage = IJsonRpcRequest | IJsonRpcNotification | IJsonRpcSuccessResponse | IJsonRpcErrorResponse;45export type JsonRpcResponse = IJsonRpcSuccessResponse | IJsonRpcErrorResponse;4647interface IPendingRequest {48promise: DeferredPromise<unknown>;49cts: CancellationTokenSource;50}5152export interface IJsonRpcProtocolHandlers {53handleRequest?(request: IJsonRpcRequest, token: CancellationToken): Promise<unknown> | unknown;54handleNotification?(notification: IJsonRpcNotification): void;55}5657export class JsonRpcError extends Error {58constructor(59public readonly code: number,60message: string,61public readonly data?: unknown,62) {63super(message);64}65}6667/**68* Generic JSON-RPC 2.0 protocol helper.69*/70export class JsonRpcProtocol extends Disposable {71private static readonly ParseError = -32700;72private static readonly MethodNotFound = -32601;73private static readonly InternalError = -32603;7475private _nextRequestId = 1;76private readonly _pendingRequests = new Map<JsonRpcId, IPendingRequest>();7778constructor(79private readonly _send: (message: JsonRpcMessage) => void,80private readonly _handlers: IJsonRpcProtocolHandlers,81) {82super();83}8485public sendNotification(notification: Omit<IJsonRpcNotification, 'jsonrpc'>): void {86this._send({87jsonrpc: '2.0',88...notification,89});90}9192public sendRequest<T = unknown>(request: Omit<IJsonRpcRequest, 'jsonrpc' | 'id'>, token: CancellationToken = CancellationToken.None, onCancel?: (id: JsonRpcId) => void): Promise<T> {93if (this._store.isDisposed) {94return Promise.reject(new CancellationError());95}9697const id = this._nextRequestId++;98const promise = new DeferredPromise<unknown>();99const cts = new CancellationTokenSource();100this._pendingRequests.set(id, { promise, cts });101102const cancelListener = token.onCancellationRequested(() => {103if (!promise.isSettled) {104this._pendingRequests.delete(id);105cts.cancel();106onCancel?.(id);107promise.cancel();108}109cancelListener.dispose();110});111112this._send({113jsonrpc: '2.0',114id,115...request,116});117118return promise.p.finally(() => {119cancelListener.dispose();120this._pendingRequests.delete(id);121cts.dispose(true);122}) as Promise<T>;123}124125/**126* Handles one or more incoming JSON-RPC messages.127*128* Returns an array of JSON-RPC response objects generated for any incoming129* requests in the message(s). Notifications and responses to our own130* outgoing requests do not produce return values. For batch inputs, the131* returned responses are in the same order as the corresponding requests.132*133* Note: responses are also emitted via the `_send` callback, so callers134* that rely on the return value should not re-send them.135*/136public async handleMessage(message: JsonRpcMessage | JsonRpcMessage[]): Promise<JsonRpcResponse[]> {137if (Array.isArray(message)) {138const replies: JsonRpcResponse[] = [];139for (const single of message) {140const reply = await this._handleMessage(single);141if (reply) {142replies.push(reply);143}144}145return replies;146}147148const reply = await this._handleMessage(message);149return reply ? [reply] : [];150}151152public cancelPendingRequest(id: JsonRpcId): void {153const request = this._pendingRequests.get(id);154if (request) {155this._pendingRequests.delete(id);156request.cts.cancel();157request.promise.cancel();158request.cts.dispose(true);159}160}161162public cancelAllRequests(): void {163for (const [id, pending] of this._pendingRequests) {164this._pendingRequests.delete(id);165pending.cts.cancel();166pending.promise.cancel();167pending.cts.dispose(true);168}169}170171private async _handleMessage(message: JsonRpcMessage): Promise<JsonRpcResponse | undefined> {172if (isJsonRpcResponse(message)) {173if (hasKey(message, { result: true })) {174this._handleResult(message);175} else {176this._handleError(message);177}178return undefined;179}180181if (isJsonRpcRequest(message)) {182return this._handleRequest(message);183}184185if (isJsonRpcNotification(message)) {186this._handlers.handleNotification?.(message);187}188189return undefined;190}191192private _handleResult(response: IJsonRpcSuccessResponse): void {193const request = this._pendingRequests.get(response.id);194if (request) {195this._pendingRequests.delete(response.id);196request.promise.complete(response.result);197request.cts.dispose(true);198}199}200201private _handleError(response: IJsonRpcErrorResponse): void {202if (response.id === undefined) {203return;204}205206const request = this._pendingRequests.get(response.id);207if (request) {208this._pendingRequests.delete(response.id);209request.promise.error(new JsonRpcError(response.error.code, response.error.message, response.error.data));210request.cts.dispose(true);211}212}213214private async _handleRequest(request: IJsonRpcRequest): Promise<JsonRpcResponse> {215if (!this._handlers.handleRequest) {216const response: IJsonRpcErrorResponse = {217jsonrpc: '2.0',218id: request.id,219error: {220code: JsonRpcProtocol.MethodNotFound,221message: `Method not found: ${request.method}`,222}223};224this._send(response);225return response;226}227228const cts = new CancellationTokenSource();229this._register(toDisposable(() => cts.dispose(true)));230231try {232const resultOrThenable = this._handlers.handleRequest(request, cts.token);233const result = isThenable(resultOrThenable) ? await resultOrThenable : resultOrThenable;234const response: IJsonRpcSuccessResponse = {235jsonrpc: '2.0',236id: request.id,237result,238};239this._send(response);240return response;241} catch (error) {242let response: IJsonRpcErrorResponse;243if (error instanceof JsonRpcError) {244response = {245jsonrpc: '2.0',246id: request.id,247error: {248code: error.code,249message: error.message,250data: error.data,251}252};253} else {254response = {255jsonrpc: '2.0',256id: request.id,257error: {258code: JsonRpcProtocol.InternalError,259message: error instanceof Error ? error.message : 'Internal error',260}261};262}263this._send(response);264return response;265} finally {266cts.dispose(true);267}268}269270public override dispose(): void {271this.cancelAllRequests();272super.dispose();273}274275public static createParseError(message: string, data?: unknown): IJsonRpcErrorResponse {276return {277jsonrpc: '2.0',278error: {279code: JsonRpcProtocol.ParseError,280message,281data,282}283};284}285}286287export function isJsonRpcRequest(message: JsonRpcMessage): message is IJsonRpcRequest {288return 'method' in message && 'id' in message && (typeof message.id === 'string' || typeof message.id === 'number');289}290291export function isJsonRpcResponse(message: JsonRpcMessage): message is IJsonRpcSuccessResponse | IJsonRpcErrorResponse {292return hasKey(message, { id: true, result: true }) || hasKey(message, { id: true, error: true });293}294295export function isJsonRpcNotification(message: JsonRpcMessage): message is IJsonRpcNotification {296return hasKey(message, { method: true }) && !hasKey(message, { id: true });297}298299300function isThenable<T>(value: T | Promise<T>): value is Promise<T> {301return typeof value === 'object' && value !== null && 'then' in value && typeof value.then === 'function';302}303304305