1 | ;
|
2 | // *****************************************************************************
|
3 | // Copyright (C) 2017 TypeFox and others.
|
4 | //
|
5 | // This program and the accompanying materials are made available under the
|
6 | // terms of the Eclipse Public License v. 2.0 which is available at
|
7 | // http://www.eclipse.org/legal/epl-2.0.
|
8 | //
|
9 | // This Source Code may also be made available under the following Secondary
|
10 | // Licenses when the conditions for such availability set forth in the Eclipse
|
11 | // Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
12 | // with the GNU Classpath Exception which is available at
|
13 | // https://www.gnu.org/software/classpath/license.html.
|
14 | //
|
15 | // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
|
16 | // *****************************************************************************
|
17 | Object.defineProperty(exports, "__esModule", { value: true });
|
18 | exports.JsonRpcProxyFactory = exports.JsonRpcConnectionHandler = void 0;
|
19 | /* eslint-disable @typescript-eslint/no-explicit-any */
|
20 | const rpc_message_encoder_1 = require("../message-rpc/rpc-message-encoder");
|
21 | const application_error_1 = require("../application-error");
|
22 | const event_1 = require("../event");
|
23 | const rpc_protocol_1 = require("../message-rpc/rpc-protocol");
|
24 | class JsonRpcConnectionHandler {
|
25 | constructor(path, targetFactory, factoryConstructor = JsonRpcProxyFactory) {
|
26 | this.path = path;
|
27 | this.targetFactory = targetFactory;
|
28 | this.factoryConstructor = factoryConstructor;
|
29 | }
|
30 | onConnection(connection) {
|
31 | const factory = new this.factoryConstructor();
|
32 | const proxy = factory.createProxy();
|
33 | factory.target = this.targetFactory(proxy);
|
34 | factory.listen(connection);
|
35 | }
|
36 | }
|
37 | exports.JsonRpcConnectionHandler = JsonRpcConnectionHandler;
|
38 | const defaultRPCConnectionFactory = (channel, requestHandler) => new rpc_protocol_1.RpcProtocol(channel, requestHandler);
|
39 | /**
|
40 | * Factory for JSON-RPC proxy objects.
|
41 | *
|
42 | * A JSON-RPC proxy exposes the programmatic interface of an object through
|
43 | * JSON-RPC. This allows remote programs to call methods of this objects by
|
44 | * sending JSON-RPC requests. This takes place over a bi-directional stream,
|
45 | * where both ends can expose an object and both can call methods each other's
|
46 | * exposed object.
|
47 | *
|
48 | * For example, assuming we have an object of the following type on one end:
|
49 | *
|
50 | * class Foo {
|
51 | * bar(baz: number): number { return baz + 1 }
|
52 | * }
|
53 | *
|
54 | * which we want to expose through a JSON-RPC interface. We would do:
|
55 | *
|
56 | * let target = new Foo()
|
57 | * let factory = new JsonRpcProxyFactory<Foo>('/foo', target)
|
58 | * factory.onConnection(connection)
|
59 | *
|
60 | * The party at the other end of the `connection`, in order to remotely call
|
61 | * methods on this object would do:
|
62 | *
|
63 | * let factory = new JsonRpcProxyFactory<Foo>('/foo')
|
64 | * factory.onConnection(connection)
|
65 | * let proxy = factory.createProxy();
|
66 | * let result = proxy.bar(42)
|
67 | * // result is equal to 43
|
68 | *
|
69 | * One the wire, it would look like this:
|
70 | *
|
71 | * --> {"jsonrpc": "2.0", "id": 0, "method": "bar", "params": {"baz": 42}}
|
72 | * <-- {"jsonrpc": "2.0", "id": 0, "result": 43}
|
73 | *
|
74 | * Note that in the code of the caller, we didn't pass a target object to
|
75 | * JsonRpcProxyFactory, because we don't want/need to expose an object.
|
76 | * If we had passed a target object, the other side could've called methods on
|
77 | * it.
|
78 | *
|
79 | * @param <T> - The type of the object to expose to JSON-RPC.
|
80 | */
|
81 | class JsonRpcProxyFactory {
|
82 | /**
|
83 | * Build a new JsonRpcProxyFactory.
|
84 | *
|
85 | * @param target - The object to expose to JSON-RPC methods calls. If this
|
86 | * is omitted, the proxy won't be able to handle requests, only send them.
|
87 | */
|
88 | constructor(target, rpcConnectionFactory = defaultRPCConnectionFactory) {
|
89 | this.target = target;
|
90 | this.rpcConnectionFactory = rpcConnectionFactory;
|
91 | this.onDidOpenConnectionEmitter = new event_1.Emitter();
|
92 | this.onDidCloseConnectionEmitter = new event_1.Emitter();
|
93 | this.waitForConnection();
|
94 | }
|
95 | waitForConnection() {
|
96 | this.connectionPromise = new Promise(resolve => this.connectionPromiseResolve = resolve);
|
97 | this.connectionPromise.then(connection => {
|
98 | connection.channel.onClose(() => {
|
99 | this.onDidCloseConnectionEmitter.fire(undefined);
|
100 | // Wait for connection in case the backend reconnects
|
101 | this.waitForConnection();
|
102 | });
|
103 | this.onDidOpenConnectionEmitter.fire(undefined);
|
104 | });
|
105 | }
|
106 | /**
|
107 | * Connect a MessageConnection to the factory.
|
108 | *
|
109 | * This connection will be used to send/receive JSON-RPC requests and
|
110 | * response.
|
111 | */
|
112 | listen(channel) {
|
113 | const connection = this.rpcConnectionFactory(channel, (meth, args) => this.onRequest(meth, ...args));
|
114 | connection.onNotification(event => this.onNotification(event.method, ...event.args));
|
115 | this.connectionPromiseResolve(connection);
|
116 | }
|
117 | /**
|
118 | * Process an incoming JSON-RPC method call.
|
119 | *
|
120 | * onRequest is called when the JSON-RPC connection received a method call
|
121 | * request. It calls the corresponding method on [[target]].
|
122 | *
|
123 | * The return value is a Promise object that is resolved with the return
|
124 | * value of the method call, if it is successful. The promise is rejected
|
125 | * if the called method does not exist or if it throws.
|
126 | *
|
127 | * @returns A promise of the method call completion.
|
128 | */
|
129 | async onRequest(method, ...args) {
|
130 | try {
|
131 | if (this.target) {
|
132 | return await this.target[method](...args);
|
133 | }
|
134 | else {
|
135 | throw new Error(`no target was set to handle ${method}`);
|
136 | }
|
137 | }
|
138 | catch (error) {
|
139 | const e = this.serializeError(error);
|
140 | if (e instanceof rpc_message_encoder_1.ResponseError) {
|
141 | throw e;
|
142 | }
|
143 | const reason = e.message || '';
|
144 | const stack = e.stack || '';
|
145 | console.error(`Request ${method} failed with error: ${reason}`, stack);
|
146 | throw e;
|
147 | }
|
148 | }
|
149 | /**
|
150 | * Process an incoming JSON-RPC notification.
|
151 | *
|
152 | * Same as [[onRequest]], but called on incoming notifications rather than
|
153 | * methods calls.
|
154 | */
|
155 | onNotification(method, ...args) {
|
156 | if (this.target) {
|
157 | this.target[method](...args);
|
158 | }
|
159 | }
|
160 | /**
|
161 | * Create a Proxy exposing the interface of an object of type T. This Proxy
|
162 | * can be used to do JSON-RPC method calls on the remote target object as
|
163 | * if it was local.
|
164 | *
|
165 | * If `T` implements `JsonRpcServer` then a client is used as a target object for a remote target object.
|
166 | */
|
167 | createProxy() {
|
168 | const result = new Proxy(this, this);
|
169 | return result;
|
170 | }
|
171 | /**
|
172 | * Get a callable object that executes a JSON-RPC method call.
|
173 | *
|
174 | * Getting a property on the Proxy object returns a callable that, when
|
175 | * called, executes a JSON-RPC call. The name of the property defines the
|
176 | * method to be called. The callable takes a variable number of arguments,
|
177 | * which are passed in the JSON-RPC method call.
|
178 | *
|
179 | * For example, if you have a Proxy object:
|
180 | *
|
181 | * let fooProxyFactory = JsonRpcProxyFactory<Foo>('/foo')
|
182 | * let fooProxy = fooProxyFactory.createProxy()
|
183 | *
|
184 | * accessing `fooProxy.bar` will return a callable that, when called,
|
185 | * executes a JSON-RPC method call to method `bar`. Therefore, doing
|
186 | * `fooProxy.bar()` will call the `bar` method on the remote Foo object.
|
187 | *
|
188 | * @param target - unused.
|
189 | * @param p - The property accessed on the Proxy object.
|
190 | * @param receiver - unused.
|
191 | * @returns A callable that executes the JSON-RPC call.
|
192 | */
|
193 | get(target, p, receiver) {
|
194 | if (p === 'setClient') {
|
195 | return (client) => {
|
196 | this.target = client;
|
197 | };
|
198 | }
|
199 | if (p === 'getClient') {
|
200 | return () => this.target;
|
201 | }
|
202 | if (p === 'onDidOpenConnection') {
|
203 | return this.onDidOpenConnectionEmitter.event;
|
204 | }
|
205 | if (p === 'onDidCloseConnection') {
|
206 | return this.onDidCloseConnectionEmitter.event;
|
207 | }
|
208 | const isNotify = this.isNotification(p);
|
209 | return (...args) => {
|
210 | const method = p.toString();
|
211 | const capturedError = new Error(`Request '${method}' failed`);
|
212 | return this.connectionPromise.then(connection => new Promise((resolve, reject) => {
|
213 | try {
|
214 | if (isNotify) {
|
215 | connection.sendNotification(method, args);
|
216 | resolve(undefined);
|
217 | }
|
218 | else {
|
219 | const resultPromise = connection.sendRequest(method, args);
|
220 | resultPromise
|
221 | .catch((err) => reject(this.deserializeError(capturedError, err)))
|
222 | .then((result) => resolve(result));
|
223 | }
|
224 | }
|
225 | catch (err) {
|
226 | reject(err);
|
227 | }
|
228 | }));
|
229 | };
|
230 | }
|
231 | /**
|
232 | * Return whether the given property represents a notification.
|
233 | *
|
234 | * A property leads to a notification rather than a method call if its name
|
235 | * begins with `notify` or `on`.
|
236 | *
|
237 | * @param p - The property being called on the proxy.
|
238 | * @return Whether `p` represents a notification.
|
239 | */
|
240 | isNotification(p) {
|
241 | return p.toString().startsWith('notify') || p.toString().startsWith('on');
|
242 | }
|
243 | serializeError(e) {
|
244 | if (application_error_1.ApplicationError.is(e)) {
|
245 | return new rpc_message_encoder_1.ResponseError(e.code, '', Object.assign({ kind: 'application' }, e.toJson()));
|
246 | }
|
247 | return e;
|
248 | }
|
249 | deserializeError(capturedError, e) {
|
250 | if (e instanceof rpc_message_encoder_1.ResponseError) {
|
251 | const capturedStack = capturedError.stack || '';
|
252 | if (e.data && e.data.kind === 'application') {
|
253 | const { stack, data, message } = e.data;
|
254 | return application_error_1.ApplicationError.fromJson(e.code, {
|
255 | message: message || capturedError.message,
|
256 | data,
|
257 | stack: `${capturedStack}\nCaused by: ${stack}`
|
258 | });
|
259 | }
|
260 | e.stack = capturedStack;
|
261 | }
|
262 | return e;
|
263 | }
|
264 | }
|
265 | exports.JsonRpcProxyFactory = JsonRpcProxyFactory;
|
266 | //# sourceMappingURL=proxy-factory.js.map |
\ | No newline at end of file |