Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/gitpod-protocol/src/messaging/proxy-factory.ts
2500 views
1
/*
2
* Copyright (C) 2017 TypeFox and others.
3
*
4
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
5
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
*/
7
8
import { MessageConnection } from "vscode-jsonrpc";
9
import { Event, Emitter } from "../util/event";
10
import { Disposable } from "../util/disposable";
11
import { ConnectionHandler } from "./handler";
12
import { log } from "../util/logging";
13
import { ApplicationError } from "./error";
14
15
export type JsonRpcServer<Client> = Disposable & {
16
/**
17
* If this server is a proxy to a remote server then
18
* a client is used as a local object
19
* to handle JSON-RPC messages from the remote server.
20
*/
21
setClient(client: Client | undefined): void;
22
};
23
24
export interface JsonRpcConnectionEventEmitter {
25
readonly onDidOpenConnection: Event<void>;
26
readonly onDidCloseConnection: Event<void>;
27
}
28
export type JsonRpcProxy<T> = T & JsonRpcConnectionEventEmitter;
29
30
export class JsonRpcConnectionHandler<T extends object> implements ConnectionHandler {
31
constructor(readonly path: string, readonly targetFactory: (proxy: JsonRpcProxy<T>, request?: object) => any) {}
32
33
onConnection(connection: MessageConnection, request?: object): void {
34
const factory = new JsonRpcProxyFactory<T>();
35
const proxy = factory.createProxy();
36
factory.target = this.targetFactory(proxy, request);
37
factory.listen(connection);
38
}
39
}
40
41
/**
42
* Factory for JSON-RPC proxy objects.
43
*
44
* A JSON-RPC proxy exposes the programmatic interface of an object through
45
* JSON-RPC. This allows remote programs to call methods of this objects by
46
* sending JSON-RPC requests. This takes place over a bi-directional stream,
47
* where both ends can expose an object and both can call methods each other's
48
* exposed object.
49
*
50
* For example, assuming we have an object of the following type on one end:
51
*
52
* class Foo {
53
* bar(baz: number): number { return baz + 1 }
54
* }
55
*
56
* which we want to expose through a JSON-RPC interface. We would do:
57
*
58
* let target = new Foo()
59
* let factory = new JsonRpcProxyFactory<Foo>('/foo', target)
60
* factory.onConnection(connection)
61
*
62
* The party at the other end of the `connection`, in order to remotely call
63
* methods on this object would do:
64
*
65
* let factory = new JsonRpcProxyFactory<Foo>('/foo')
66
* factory.onConnection(connection)
67
* let proxy = factory.createProxy();
68
* let result = proxy.bar(42)
69
* // result is equal to 43
70
*
71
* One the wire, it would look like this:
72
*
73
* --> {"jsonrpc": "2.0", "id": 0, "method": "bar", "params": {"baz": 42}}
74
* <-- {"jsonrpc": "2.0", "id": 0, "result": 43}
75
*
76
* Note that in the code of the caller, we didn't pass a target object to
77
* JsonRpcProxyFactory, because we don't want/need to expose an object.
78
* If we had passed a target object, the other side could've called methods on
79
* it.
80
*
81
* @param <T> - The type of the object to expose to JSON-RPC.
82
*/
83
export class JsonRpcProxyFactory<T extends object> implements ProxyHandler<T> {
84
protected readonly onDidOpenConnectionEmitter = new Emitter<void>();
85
protected readonly onDidCloseConnectionEmitter = new Emitter<void>();
86
87
protected connectionPromiseResolve: (connection: MessageConnection) => void;
88
protected connectionPromise: Promise<MessageConnection>;
89
90
/**
91
* Build a new JsonRpcProxyFactory.
92
*
93
* @param target - The object to expose to JSON-RPC methods calls. If this
94
* is omitted, the proxy won't be able to handle requests, only send them.
95
*/
96
constructor(public target?: any) {
97
this.waitForConnection();
98
}
99
100
protected waitForConnection(): void {
101
this.connectionPromise = new Promise((resolve) => (this.connectionPromiseResolve = resolve));
102
this.connectionPromise
103
.then((connection) => {
104
connection.onClose(() => this.fireConnectionClosed());
105
this.fireConnectionOpened();
106
})
107
.catch((err) => {
108
log.error("Error while waiting for connection", err);
109
});
110
}
111
112
fireConnectionClosed() {
113
this.onDidCloseConnectionEmitter.fire(undefined);
114
}
115
116
fireConnectionOpened() {
117
this.onDidOpenConnectionEmitter.fire(undefined);
118
}
119
120
/**
121
* Connect a MessageConnection to the factory.
122
*
123
* This connection will be used to send/receive JSON-RPC requests and
124
* response.
125
*/
126
listen(connection: MessageConnection) {
127
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
128
connection.onRequest((method: string, ...params: any[]) => this.onRequest(method, ...params));
129
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
130
connection.onNotification((method: string, ...params: any[]) => this.onNotification(method, ...params));
131
connection.onDispose(() => this.waitForConnection());
132
connection.listen();
133
this.connectionPromiseResolve(connection);
134
}
135
136
/**
137
* Process an incoming JSON-RPC method call.
138
*
139
* onRequest is called when the JSON-RPC connection received a method call
140
* request. It calls the corresponding method on [[target]].
141
*
142
* The return value is a Promise object that is resolved with the return
143
* value of the method call, if it is successful. The promise is rejected
144
* if the called method does not exist or if it throws.
145
*
146
* @returns A promise of the method call completion.
147
*/
148
protected async onRequest(method: string, ...args: any[]): Promise<any> {
149
try {
150
return await this.target[method](...args);
151
} catch (e) {
152
if (ApplicationError.hasErrorCode(e)) {
153
log.info(`Request ${method} unsuccessful: ${e.code}/"${e.message}"`, { method, args });
154
} else {
155
log.error(`Request ${method} failed with internal server error`, e, { method, args });
156
}
157
throw e;
158
}
159
}
160
161
/**
162
* Process an incoming JSON-RPC notification.
163
*
164
* Same as [[onRequest]], but called on incoming notifications rather than
165
* methods calls.
166
*/
167
protected onNotification(method: string, ...args: any[]): void {
168
if (this.target[method]) {
169
this.target[method](...args);
170
}
171
}
172
173
/**
174
* Create a Proxy exposing the interface of an object of type T. This Proxy
175
* can be used to do JSON-RPC method calls on the remote target object as
176
* if it was local.
177
*
178
* If `T` implements `JsonRpcServer` then a client is used as a target object for a remote target object.
179
*/
180
createProxy(): JsonRpcProxy<T> {
181
const result = new Proxy<T>(this as unknown as T, this);
182
return result as any;
183
}
184
185
/**
186
* Get a callable object that executes a JSON-RPC method call.
187
*
188
* Getting a property on the Proxy object returns a callable that, when
189
* called, executes a JSON-RPC call. The name of the property defines the
190
* method to be called. The callable takes a variable number of arguments,
191
* which are passed in the JSON-RPC method call.
192
*
193
* For example, if you have a Proxy object:
194
*
195
* let fooProxyFactory = JsonRpcProxyFactory<Foo>('/foo')
196
* let fooProxy = fooProxyFactory.createProxy()
197
*
198
* accessing `fooProxy.bar` will return a callable that, when called,
199
* executes a JSON-RPC method call to method `bar`. Therefore, doing
200
* `fooProxy.bar()` will call the `bar` method on the remote Foo object.
201
*
202
* @param target - unused.
203
* @param p - The property accessed on the Proxy object.
204
* @param receiver - unused.
205
* @returns A callable that executes the JSON-RPC call.
206
*/
207
get(target: T, p: PropertyKey, receiver: any): any {
208
if (p === "setClient") {
209
return (client: any) => {
210
this.target = client;
211
};
212
}
213
if (p === "onDidOpenConnection") {
214
return this.onDidOpenConnectionEmitter.event;
215
}
216
if (p === "onDidCloseConnection") {
217
return this.onDidCloseConnectionEmitter.event;
218
}
219
const isNotify = this.isNotification(p);
220
return (...args: any[]) =>
221
this.connectionPromise.then(
222
(connection) =>
223
new Promise((resolve, reject) => {
224
try {
225
if (isNotify) {
226
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
227
connection.sendNotification(p.toString(), ...args);
228
resolve(undefined);
229
} else {
230
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
231
const resultPromise = connection.sendRequest(p.toString(), ...args) as Promise<any>;
232
resultPromise.then(resolve, reject);
233
}
234
} catch (err) {
235
reject(err);
236
}
237
}),
238
);
239
}
240
241
/**
242
* Return whether the given property represents a notification.
243
*
244
* A property leads to a notification rather than a method call if its name
245
* begins with `notify` or `on`.
246
*
247
* @param p - The property being called on the proxy.
248
* @return Whether `p` represents a notification.
249
*/
250
protected isNotification(p: PropertyKey): boolean {
251
return p.toString().startsWith("notify") || p.toString().startsWith("on");
252
}
253
}
254
255