UNPKG

10.7 kBJavaScriptView Raw
1"use strict";
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// *****************************************************************************
17Object.defineProperty(exports, "__esModule", { value: true });
18exports.JsonRpcProxyFactory = exports.JsonRpcConnectionHandler = void 0;
19/* eslint-disable @typescript-eslint/no-explicit-any */
20const rpc_message_encoder_1 = require("../message-rpc/rpc-message-encoder");
21const application_error_1 = require("../application-error");
22const event_1 = require("../event");
23const rpc_protocol_1 = require("../message-rpc/rpc-protocol");
24class 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}
37exports.JsonRpcConnectionHandler = JsonRpcConnectionHandler;
38const 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 */
81class 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}
265exports.JsonRpcProxyFactory = JsonRpcProxyFactory;
266//# sourceMappingURL=proxy-factory.js.map
\No newline at end of file