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-only WITH Classpath-exception-2.0
|
16 | // *****************************************************************************
|
17 | Object.defineProperty(exports, "__esModule", { value: true });
|
18 | exports.JsonRpcProxyFactory = exports.JsonRpcConnectionHandler = exports.RpcProxyFactory = exports.RpcConnectionHandler = 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 | const promise_util_1 = require("../promise-util");
|
25 | const inversify_1 = require("../../../shared/inversify");
|
26 | class RpcConnectionHandler {
|
27 | constructor(path, targetFactory, factoryConstructor = RpcProxyFactory) {
|
28 | this.path = path;
|
29 | this.targetFactory = targetFactory;
|
30 | this.factoryConstructor = factoryConstructor;
|
31 | }
|
32 | onConnection(connection) {
|
33 | const factory = new this.factoryConstructor();
|
34 | const proxy = factory.createProxy();
|
35 | factory.target = this.targetFactory(proxy);
|
36 | factory.listen(connection);
|
37 | }
|
38 | }
|
39 | exports.RpcConnectionHandler = RpcConnectionHandler;
|
40 | const defaultRpcProtocolFactory = (channel, requestHandler) => new rpc_protocol_1.RpcProtocol(channel, requestHandler);
|
41 | /**
|
42 | * Factory for RPC proxy objects.
|
43 | *
|
44 | * A RPC proxy exposes the programmatic interface of an object through
|
45 | * Theia's RPC protocol. This allows remote programs to call methods of this objects by
|
46 | * sending RPC requests. This takes place over a bi-directional stream,
|
47 | * where both ends can expose an object and both can call methods on each other'
|
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 RPC interface. We would do:
|
57 | *
|
58 | * let target = new Foo()
|
59 | * let factory = new RpcProxyFactory<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 RpcProxyFactory<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 | * --> { "type":"1", "id": 1, "method": "bar", "args": [42]}
|
74 | * <-- { "type":"3", "id": 1, "res": 43}
|
75 | *
|
76 | * Note that in the code of the caller, we didn't pass a target object to
|
77 | * RpcProxyFactory, 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 RPC.
|
82 | */
|
83 | class RpcProxyFactory {
|
84 | /**
|
85 | * Build a new RpcProxyFactory.
|
86 | *
|
87 | * @param target - The object to expose to RPC methods calls. If this
|
88 | * is omitted, the proxy won't be able to handle requests, only send them.
|
89 | */
|
90 | constructor(target, rpcProtocolFactory = defaultRpcProtocolFactory) {
|
91 | this.target = target;
|
92 | this.rpcProtocolFactory = rpcProtocolFactory;
|
93 | this.onDidOpenConnectionEmitter = new event_1.Emitter();
|
94 | this.onDidCloseConnectionEmitter = new event_1.Emitter();
|
95 | this.waitForConnection();
|
96 | }
|
97 | waitForConnection() {
|
98 | this.rpcDeferred = new promise_util_1.Deferred();
|
99 | this.rpcDeferred.promise.then(protocol => {
|
100 | protocol.channel.onClose(() => {
|
101 | this.onDidCloseConnectionEmitter.fire(undefined);
|
102 | // Wait for connection in case the backend reconnects
|
103 | this.waitForConnection();
|
104 | });
|
105 | this.onDidOpenConnectionEmitter.fire(undefined);
|
106 | });
|
107 | }
|
108 | /**
|
109 | * Connect a {@link Channel} to the factory by creating an {@link RpcProtocol} on top of it.
|
110 | *
|
111 | * This protocol will be used to send/receive RPC requests and
|
112 | * responses.
|
113 | */
|
114 | listen(channel) {
|
115 | const protocol = this.rpcProtocolFactory(channel, (meth, args) => this.onRequest(meth, ...args));
|
116 | protocol.onNotification(event => this.onNotification(event.method, ...event.args));
|
117 | this.rpcDeferred.resolve(protocol);
|
118 | }
|
119 | /**
|
120 | * Process an incoming RPC method call.
|
121 | *
|
122 | * onRequest is called when the RPC connection received a method call
|
123 | * request. It calls the corresponding method on [[target]].
|
124 | *
|
125 | * The return value is a Promise object that is resolved with the return
|
126 | * value of the method call, if it is successful. The promise is rejected
|
127 | * if the called method does not exist or if it throws.
|
128 | *
|
129 | * @returns A promise of the method call completion.
|
130 | */
|
131 | async onRequest(method, ...args) {
|
132 | try {
|
133 | if (this.target) {
|
134 | return await this.target[method](...args);
|
135 | }
|
136 | else {
|
137 | throw new Error(`no target was set to handle ${method}`);
|
138 | }
|
139 | }
|
140 | catch (error) {
|
141 | const e = this.serializeError(error);
|
142 | if (e instanceof rpc_message_encoder_1.ResponseError) {
|
143 | throw e;
|
144 | }
|
145 | const reason = e.message || '';
|
146 | const stack = e.stack || '';
|
147 | console.error(`Request ${method} failed with error: ${reason}`, stack);
|
148 | throw e;
|
149 | }
|
150 | }
|
151 | /**
|
152 | * Process an incoming RPC notification.
|
153 | *
|
154 | * Same as [[onRequest]], but called on incoming notifications rather than
|
155 | * methods calls.
|
156 | */
|
157 | onNotification(method, ...args) {
|
158 | if (this.target) {
|
159 | this.target[method](...args);
|
160 | }
|
161 | }
|
162 | /**
|
163 | * Create a Proxy exposing the interface of an object of type T. This Proxy
|
164 | * can be used to do RPC method calls on the remote target object as
|
165 | * if it was local.
|
166 | *
|
167 | * If `T` implements `RpcServer` then a client is used as a target object for a remote target object.
|
168 | */
|
169 | createProxy() {
|
170 | const result = new Proxy(this, this);
|
171 | return result;
|
172 | }
|
173 | /**
|
174 | * Get a callable object that executes a RPC method call.
|
175 | *
|
176 | * Getting a property on the Proxy object returns a callable that, when
|
177 | * called, executes a RPC call. The name of the property defines the
|
178 | * method to be called. The callable takes a variable number of arguments,
|
179 | * which are passed in the RPC method call.
|
180 | *
|
181 | * For example, if you have a Proxy object:
|
182 | *
|
183 | * let fooProxyFactory = RpcProxyFactory<Foo>('/foo')
|
184 | * let fooProxy = fooProxyFactory.createProxy()
|
185 | *
|
186 | * accessing `fooProxy.bar` will return a callable that, when called,
|
187 | * executes a RPC method call to method `bar`. Therefore, doing
|
188 | * `fooProxy.bar()` will call the `bar` method on the remote Foo object.
|
189 | *
|
190 | * @param target - unused.
|
191 | * @param p - The property accessed on the Proxy object.
|
192 | * @param receiver - unused.
|
193 | * @returns A callable that executes the RPC call.
|
194 | */
|
195 | get(target, p, receiver) {
|
196 | if (p === 'setClient') {
|
197 | return (client) => {
|
198 | this.target = client;
|
199 | };
|
200 | }
|
201 | if (p === 'getClient') {
|
202 | return () => this.target;
|
203 | }
|
204 | if (p === 'onDidOpenConnection') {
|
205 | return this.onDidOpenConnectionEmitter.event;
|
206 | }
|
207 | if (p === 'onDidCloseConnection') {
|
208 | return this.onDidCloseConnectionEmitter.event;
|
209 | }
|
210 | if (p === 'then') {
|
211 | // Prevent inversify from identifying this proxy as a promise object.
|
212 | return undefined;
|
213 | }
|
214 | const isNotify = this.isNotification(p);
|
215 | return (...args) => {
|
216 | const method = p.toString();
|
217 | const capturedError = new Error(`Request '${method}' failed`);
|
218 | return this.rpcDeferred.promise.then(connection => new Promise((resolve, reject) => {
|
219 | try {
|
220 | if (isNotify) {
|
221 | connection.sendNotification(method, args);
|
222 | resolve(undefined);
|
223 | }
|
224 | else {
|
225 | const resultPromise = connection.sendRequest(method, args);
|
226 | resultPromise
|
227 | .catch((err) => reject(this.deserializeError(capturedError, err)))
|
228 | .then((result) => resolve(result));
|
229 | }
|
230 | }
|
231 | catch (err) {
|
232 | reject(err);
|
233 | }
|
234 | }));
|
235 | };
|
236 | }
|
237 | /**
|
238 | * Return whether the given property represents a notification.
|
239 | *
|
240 | * A property leads to a notification rather than a method call if its name
|
241 | * begins with `notify` or `on`.
|
242 | *
|
243 | * @param p - The property being called on the proxy.
|
244 | * @return Whether `p` represents a notification.
|
245 | */
|
246 | isNotification(p) {
|
247 | return p.toString().startsWith('notify') || p.toString().startsWith('on');
|
248 | }
|
249 | serializeError(e) {
|
250 | if (application_error_1.ApplicationError.is(e)) {
|
251 | return new rpc_message_encoder_1.ResponseError(e.code, '', Object.assign({ kind: 'application' }, e.toJson()));
|
252 | }
|
253 | return e;
|
254 | }
|
255 | deserializeError(capturedError, e) {
|
256 | if (e instanceof rpc_message_encoder_1.ResponseError) {
|
257 | const capturedStack = capturedError.stack || '';
|
258 | if (e.data && e.data.kind === 'application') {
|
259 | const { stack, data, message } = e.data;
|
260 | return application_error_1.ApplicationError.fromJson(e.code, {
|
261 | message: message || capturedError.message,
|
262 | data,
|
263 | stack: `${capturedStack}\nCaused by: ${stack}`
|
264 | });
|
265 | }
|
266 | e.stack = capturedStack;
|
267 | }
|
268 | return e;
|
269 | }
|
270 | }
|
271 | exports.RpcProxyFactory = RpcProxyFactory;
|
272 | /**
|
273 | * @deprecated since 1.39.0 use `RpcConnectionHandler` instead
|
274 | */
|
275 | class JsonRpcConnectionHandler extends RpcConnectionHandler {
|
276 | }
|
277 | exports.JsonRpcConnectionHandler = JsonRpcConnectionHandler;
|
278 | /**
|
279 | * @deprecated since 1.39.0 use `RpcProxyFactory` instead
|
280 | */
|
281 | class JsonRpcProxyFactory extends RpcProxyFactory {
|
282 | }
|
283 | exports.JsonRpcProxyFactory = JsonRpcProxyFactory;
|
284 | // eslint-disable-next-line deprecation/deprecation
|
285 | (0, inversify_1.decorate)((0, inversify_1.injectable)(), JsonRpcProxyFactory);
|
286 | // eslint-disable-next-line deprecation/deprecation
|
287 | (0, inversify_1.decorate)((0, inversify_1.unmanaged)(), JsonRpcProxyFactory, 0);
|
288 | //# sourceMappingURL=proxy-factory.js.map |
\ | No newline at end of file |