Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/common/jsonRpcProtocol.ts
13389 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { DeferredPromise } from './async.js';
7
import { CancellationToken, CancellationTokenSource } from './cancellation.js';
8
import { CancellationError } from './errors.js';
9
import { Disposable, toDisposable } from './lifecycle.js';
10
import { hasKey } from './types.js';
11
12
export type JsonRpcId = string | number;
13
14
export interface IJsonRpcError {
15
code: number;
16
message: string;
17
data?: unknown;
18
}
19
20
export interface IJsonRpcRequest {
21
jsonrpc: '2.0';
22
id: JsonRpcId;
23
method: string;
24
params?: unknown;
25
}
26
27
export interface IJsonRpcNotification {
28
jsonrpc: '2.0';
29
method: string;
30
params?: unknown;
31
}
32
33
export interface IJsonRpcSuccessResponse {
34
jsonrpc: '2.0';
35
id: JsonRpcId;
36
result: unknown;
37
}
38
39
export interface IJsonRpcErrorResponse {
40
jsonrpc: '2.0';
41
id?: JsonRpcId;
42
error: IJsonRpcError;
43
}
44
45
export type JsonRpcMessage = IJsonRpcRequest | IJsonRpcNotification | IJsonRpcSuccessResponse | IJsonRpcErrorResponse;
46
export type JsonRpcResponse = IJsonRpcSuccessResponse | IJsonRpcErrorResponse;
47
48
interface IPendingRequest {
49
promise: DeferredPromise<unknown>;
50
cts: CancellationTokenSource;
51
}
52
53
export interface IJsonRpcProtocolHandlers {
54
handleRequest?(request: IJsonRpcRequest, token: CancellationToken): Promise<unknown> | unknown;
55
handleNotification?(notification: IJsonRpcNotification): void;
56
}
57
58
export class JsonRpcError extends Error {
59
constructor(
60
public readonly code: number,
61
message: string,
62
public readonly data?: unknown,
63
) {
64
super(message);
65
}
66
}
67
68
/**
69
* Generic JSON-RPC 2.0 protocol helper.
70
*/
71
export class JsonRpcProtocol extends Disposable {
72
private static readonly ParseError = -32700;
73
private static readonly MethodNotFound = -32601;
74
private static readonly InternalError = -32603;
75
76
private _nextRequestId = 1;
77
private readonly _pendingRequests = new Map<JsonRpcId, IPendingRequest>();
78
79
constructor(
80
private readonly _send: (message: JsonRpcMessage) => void,
81
private readonly _handlers: IJsonRpcProtocolHandlers,
82
) {
83
super();
84
}
85
86
public sendNotification(notification: Omit<IJsonRpcNotification, 'jsonrpc'>): void {
87
this._send({
88
jsonrpc: '2.0',
89
...notification,
90
});
91
}
92
93
public sendRequest<T = unknown>(request: Omit<IJsonRpcRequest, 'jsonrpc' | 'id'>, token: CancellationToken = CancellationToken.None, onCancel?: (id: JsonRpcId) => void): Promise<T> {
94
if (this._store.isDisposed) {
95
return Promise.reject(new CancellationError());
96
}
97
98
const id = this._nextRequestId++;
99
const promise = new DeferredPromise<unknown>();
100
const cts = new CancellationTokenSource();
101
this._pendingRequests.set(id, { promise, cts });
102
103
const cancelListener = token.onCancellationRequested(() => {
104
if (!promise.isSettled) {
105
this._pendingRequests.delete(id);
106
cts.cancel();
107
onCancel?.(id);
108
promise.cancel();
109
}
110
cancelListener.dispose();
111
});
112
113
this._send({
114
jsonrpc: '2.0',
115
id,
116
...request,
117
});
118
119
return promise.p.finally(() => {
120
cancelListener.dispose();
121
this._pendingRequests.delete(id);
122
cts.dispose(true);
123
}) as Promise<T>;
124
}
125
126
/**
127
* Handles one or more incoming JSON-RPC messages.
128
*
129
* Returns an array of JSON-RPC response objects generated for any incoming
130
* requests in the message(s). Notifications and responses to our own
131
* outgoing requests do not produce return values. For batch inputs, the
132
* returned responses are in the same order as the corresponding requests.
133
*
134
* Note: responses are also emitted via the `_send` callback, so callers
135
* that rely on the return value should not re-send them.
136
*/
137
public async handleMessage(message: JsonRpcMessage | JsonRpcMessage[]): Promise<JsonRpcResponse[]> {
138
if (Array.isArray(message)) {
139
const replies: JsonRpcResponse[] = [];
140
for (const single of message) {
141
const reply = await this._handleMessage(single);
142
if (reply) {
143
replies.push(reply);
144
}
145
}
146
return replies;
147
}
148
149
const reply = await this._handleMessage(message);
150
return reply ? [reply] : [];
151
}
152
153
public cancelPendingRequest(id: JsonRpcId): void {
154
const request = this._pendingRequests.get(id);
155
if (request) {
156
this._pendingRequests.delete(id);
157
request.cts.cancel();
158
request.promise.cancel();
159
request.cts.dispose(true);
160
}
161
}
162
163
public cancelAllRequests(): void {
164
for (const [id, pending] of this._pendingRequests) {
165
this._pendingRequests.delete(id);
166
pending.cts.cancel();
167
pending.promise.cancel();
168
pending.cts.dispose(true);
169
}
170
}
171
172
private async _handleMessage(message: JsonRpcMessage): Promise<JsonRpcResponse | undefined> {
173
if (isJsonRpcResponse(message)) {
174
if (hasKey(message, { result: true })) {
175
this._handleResult(message);
176
} else {
177
this._handleError(message);
178
}
179
return undefined;
180
}
181
182
if (isJsonRpcRequest(message)) {
183
return this._handleRequest(message);
184
}
185
186
if (isJsonRpcNotification(message)) {
187
this._handlers.handleNotification?.(message);
188
}
189
190
return undefined;
191
}
192
193
private _handleResult(response: IJsonRpcSuccessResponse): void {
194
const request = this._pendingRequests.get(response.id);
195
if (request) {
196
this._pendingRequests.delete(response.id);
197
request.promise.complete(response.result);
198
request.cts.dispose(true);
199
}
200
}
201
202
private _handleError(response: IJsonRpcErrorResponse): void {
203
if (response.id === undefined) {
204
return;
205
}
206
207
const request = this._pendingRequests.get(response.id);
208
if (request) {
209
this._pendingRequests.delete(response.id);
210
request.promise.error(new JsonRpcError(response.error.code, response.error.message, response.error.data));
211
request.cts.dispose(true);
212
}
213
}
214
215
private async _handleRequest(request: IJsonRpcRequest): Promise<JsonRpcResponse> {
216
if (!this._handlers.handleRequest) {
217
const response: IJsonRpcErrorResponse = {
218
jsonrpc: '2.0',
219
id: request.id,
220
error: {
221
code: JsonRpcProtocol.MethodNotFound,
222
message: `Method not found: ${request.method}`,
223
}
224
};
225
this._send(response);
226
return response;
227
}
228
229
const cts = new CancellationTokenSource();
230
this._register(toDisposable(() => cts.dispose(true)));
231
232
try {
233
const resultOrThenable = this._handlers.handleRequest(request, cts.token);
234
const result = isThenable(resultOrThenable) ? await resultOrThenable : resultOrThenable;
235
const response: IJsonRpcSuccessResponse = {
236
jsonrpc: '2.0',
237
id: request.id,
238
result,
239
};
240
this._send(response);
241
return response;
242
} catch (error) {
243
let response: IJsonRpcErrorResponse;
244
if (error instanceof JsonRpcError) {
245
response = {
246
jsonrpc: '2.0',
247
id: request.id,
248
error: {
249
code: error.code,
250
message: error.message,
251
data: error.data,
252
}
253
};
254
} else {
255
response = {
256
jsonrpc: '2.0',
257
id: request.id,
258
error: {
259
code: JsonRpcProtocol.InternalError,
260
message: error instanceof Error ? error.message : 'Internal error',
261
}
262
};
263
}
264
this._send(response);
265
return response;
266
} finally {
267
cts.dispose(true);
268
}
269
}
270
271
public override dispose(): void {
272
this.cancelAllRequests();
273
super.dispose();
274
}
275
276
public static createParseError(message: string, data?: unknown): IJsonRpcErrorResponse {
277
return {
278
jsonrpc: '2.0',
279
error: {
280
code: JsonRpcProtocol.ParseError,
281
message,
282
data,
283
}
284
};
285
}
286
}
287
288
export function isJsonRpcRequest(message: JsonRpcMessage): message is IJsonRpcRequest {
289
return 'method' in message && 'id' in message && (typeof message.id === 'string' || typeof message.id === 'number');
290
}
291
292
export function isJsonRpcResponse(message: JsonRpcMessage): message is IJsonRpcSuccessResponse | IJsonRpcErrorResponse {
293
return hasKey(message, { id: true, result: true }) || hasKey(message, { id: true, error: true });
294
}
295
296
export function isJsonRpcNotification(message: JsonRpcMessage): message is IJsonRpcNotification {
297
return hasKey(message, { method: true }) && !hasKey(message, { id: true });
298
}
299
300
301
function isThenable<T>(value: T | Promise<T>): value is Promise<T> {
302
return typeof value === 'object' && value !== null && 'then' in value && typeof value.then === 'function';
303
}
304
305