@celljs/rpc-core 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/rpc.ts ADDED
@@ -0,0 +1,352 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ import { DisposableCollection, Disposable } from '@celljs/core/lib/common/utils/disposable';
4
+ import { Deferred } from '@celljs/core/lib/common/utils/promise-util';
5
+ import { CancellationToken, CancellationTokenSource } from '@celljs/core/lib/common/utils/cancellation';
6
+ import { CancelMessage, ConnectionClosedError, MessageConnection, MessageType, ObjectsTransferrer, OpenMessage,
7
+ ProxyIdentifier, RPC, RPCMessage, ReadyMessage, ReplyErrMessage, ReplyMessage, RequestMessage, SerializedError } from './rpc-protocol';
8
+ import { RPCMultiplexer } from './rpc-multiplexer';
9
+
10
+ function isBoolean(value: any): value is boolean {
11
+ return value === true || value === false;
12
+ }
13
+
14
+ function isCancellationToken(value: any): value is CancellationToken {
15
+ const candidate = value as CancellationToken;
16
+ return (
17
+ candidate &&
18
+ (candidate === CancellationToken.None ||
19
+ candidate === CancellationToken.Cancelled ||
20
+ (isBoolean(candidate.isCancellationRequested) && !!candidate.onCancellationRequested))
21
+ );
22
+ }
23
+
24
+ export class RPCImpl implements RPC {
25
+ private readonly locals = new Map<string, any>();
26
+ private readonly proxies = new Map<string, any>();
27
+ private lastMessageId = 0;
28
+ private readonly cancellationTokenSources = new Map<string, CancellationTokenSource>();
29
+ private readonly pendingRPCReplies = new Map<string, Deferred<any>>();
30
+ private readonly multiplexer: RPCMultiplexer;
31
+ private readonly readyDeferred = new Deferred<void>();
32
+
33
+ private replacer: (key: string | undefined, value: any) => any;
34
+ private reviver: (key: string | undefined, value: any) => any;
35
+
36
+ private readonly toDispose = new DisposableCollection(
37
+ Disposable.create(() => {
38
+ /* mark as no disposed */
39
+ })
40
+ );
41
+
42
+ constructor(
43
+ connection: MessageConnection,
44
+ transformations?: {
45
+ replacer?: (key: string | undefined, value: any) => any;
46
+ reviver?: (key: string | undefined, value: any) => any;
47
+ }
48
+ ) {
49
+ this.toDispose.push((this.multiplexer = new RPCMultiplexer(connection)));
50
+ this.multiplexer.onMessage(msg => this.receiveOneMessage(msg));
51
+ this.toDispose.push(
52
+ Disposable.create(() => {
53
+ this.proxies.clear();
54
+ for (const reply of this.pendingRPCReplies.values()) {
55
+ reply.reject(ConnectionClosedError.create());
56
+ }
57
+ this.pendingRPCReplies.clear();
58
+ })
59
+ );
60
+
61
+ this.reviver = transformations?.reviver || ObjectsTransferrer.reviver;
62
+ this.replacer = transformations?.replacer || ObjectsTransferrer.replacer;
63
+ this.connect();
64
+ }
65
+
66
+ protected async connect(): Promise<void> {
67
+ let count = 0;
68
+ const task = setInterval(() => {
69
+ const callId = String(++this.lastMessageId);
70
+ this.multiplexer.send(this.open(callId));
71
+ count++;
72
+ if (count > 300) {
73
+ clearInterval(task);
74
+ }
75
+ }, 500);
76
+ await this.readyDeferred.promise;
77
+ clearInterval(task);
78
+ }
79
+
80
+ private get isDisposed(): boolean {
81
+ return this.toDispose.disposed;
82
+ }
83
+
84
+ dispose(): void {
85
+ this.toDispose.dispose();
86
+ }
87
+
88
+ getProxy<T>(proxyId: ProxyIdentifier<T>): T {
89
+ if (this.isDisposed) {
90
+ throw ConnectionClosedError.create();
91
+ }
92
+ let proxy = this.proxies.get(proxyId.id);
93
+ if (!proxy) {
94
+ proxy = this.createProxy(proxyId.id);
95
+ this.proxies.set(proxyId.id, proxy);
96
+ }
97
+ return proxy;
98
+ }
99
+
100
+ getLocal<T>(proxyId: ProxyIdentifier<T>): T {
101
+ if (this.isDisposed) {
102
+ throw ConnectionClosedError.create();
103
+ }
104
+ return this.locals.get(proxyId.id) as T;
105
+ }
106
+
107
+ set<T, R extends T>(identifier: ProxyIdentifier<T>, instance: R): R {
108
+ if (this.isDisposed) {
109
+ throw ConnectionClosedError.create();
110
+ }
111
+ this.locals.set(identifier.id, instance);
112
+ if (Disposable.is(instance)) {
113
+ this.toDispose.push(instance);
114
+ }
115
+ this.toDispose.push(Disposable.create(() => this.locals.delete(identifier.id)));
116
+ return instance;
117
+ }
118
+
119
+ private createProxy<T>(proxyId: string): T {
120
+ const handler = {
121
+ get: (target: any, name: string) => {
122
+ if (!target[name]) {
123
+ target[name] = (...myArgs: any[]) => this.remoteCall(proxyId, name, myArgs);
124
+ }
125
+ return target[name];
126
+ },
127
+ };
128
+ return new Proxy(Object.create(null), handler); // eslint-disable-line no-null/no-null
129
+ }
130
+
131
+ private remoteCall(proxyId: string, methodName: string, args: any[]): Promise<any> {
132
+
133
+ if (this.isDisposed) {
134
+ return Promise.reject(ConnectionClosedError.create());
135
+ }
136
+ return this.readyDeferred.promise.then(() => {
137
+ const cancellationToken: CancellationToken | undefined =
138
+ args.length && isCancellationToken(args[args.length - 1]) ? args.pop() : undefined;
139
+ if (cancellationToken && cancellationToken.isCancellationRequested) {
140
+ return Promise.reject(canceled());
141
+ }
142
+
143
+ const callId = String(++this.lastMessageId);
144
+ const result = new Deferred();
145
+
146
+ if (cancellationToken) {
147
+ args.push('add.cancellation.token');
148
+ cancellationToken.onCancellationRequested(() => this.multiplexer.send(this.cancel(callId)));
149
+ }
150
+
151
+ this.pendingRPCReplies.set(callId, result);
152
+ this.multiplexer.send(this.request(callId, proxyId, methodName, args));
153
+ return result.promise;
154
+ });
155
+
156
+ }
157
+
158
+ private receiveOneMessage(rawmsg: string): void {
159
+ if (this.isDisposed) {
160
+ return;
161
+ }
162
+ try {
163
+ const msg = <RPCMessage>JSON.parse(rawmsg, this.reviver);
164
+
165
+ switch (msg.type) {
166
+ case MessageType.Request:
167
+ this.receiveRequest(msg);
168
+ break;
169
+ case MessageType.Reply:
170
+ this.receiveReply(msg);
171
+ break;
172
+ case MessageType.ReplyErr:
173
+ this.receiveReplyErr(msg);
174
+ break;
175
+ case MessageType.Cancel:
176
+ this.receiveCancel(msg);
177
+ break;
178
+ case MessageType.Open:
179
+ this.receiveOpen(msg);
180
+ break;
181
+ case MessageType.Ready:
182
+ this.receiveReady(msg);
183
+ break;
184
+ }
185
+ } catch (e) {
186
+ // exception does not show problematic content: log it!
187
+ console.log('failed to parse message: ' + rawmsg);
188
+ throw e;
189
+ }
190
+ }
191
+
192
+ private receiveOpen(msg: OpenMessage): void {
193
+ this.multiplexer.send(this.replyReady(msg.id));
194
+ }
195
+
196
+ private receiveReady(msg: ReadyMessage): void {
197
+ this.readyDeferred.resolve();
198
+ }
199
+
200
+ private receiveCancel(msg: CancelMessage): void {
201
+ const cancellationTokenSource = this.cancellationTokenSources.get(msg.id);
202
+ if (cancellationTokenSource) {
203
+ cancellationTokenSource.cancel();
204
+ }
205
+ }
206
+
207
+ private receiveRequest(msg: RequestMessage): void {
208
+ const callId = msg.id;
209
+ const proxyId = msg.proxyId;
210
+ // convert `null` to `undefined`, since we don't use `null` in internal plugin APIs
211
+ const args = msg.args.map(arg => (arg === null ? undefined : arg)); // eslint-disable-line no-null/no-null
212
+
213
+ const addToken = args.length && args[args.length - 1] === 'add.cancellation.token' ? args.pop() : false;
214
+ if (addToken) {
215
+ const tokenSource = new CancellationTokenSource();
216
+ this.cancellationTokenSources.set(callId, tokenSource);
217
+ args.push(tokenSource.token);
218
+ }
219
+ const invocation = this.invokeHandler(proxyId, msg.method, args);
220
+
221
+ invocation.then(
222
+ result => {
223
+ this.cancellationTokenSources.delete(callId);
224
+ this.multiplexer.send(this.replyOK(callId, result));
225
+ },
226
+ error => {
227
+ this.cancellationTokenSources.delete(callId);
228
+ this.multiplexer.send(this.replyErr(callId, error));
229
+ }
230
+ );
231
+ }
232
+
233
+ private receiveReply(msg: ReplyMessage): void {
234
+ const callId = msg.id;
235
+ const pendingReply = this.pendingRPCReplies.get(callId);
236
+ if (!pendingReply) {
237
+ return;
238
+ }
239
+ this.pendingRPCReplies.delete(callId);
240
+ pendingReply.resolve(msg.res);
241
+ }
242
+
243
+ private receiveReplyErr(msg: ReplyErrMessage): void {
244
+ const callId = msg.id;
245
+ const pendingReply = this.pendingRPCReplies.get(callId);
246
+ if (!pendingReply) {
247
+ return;
248
+ }
249
+ this.pendingRPCReplies.delete(callId);
250
+
251
+ let err: Error | undefined = undefined;
252
+ if (msg.err && msg.err.$isError) {
253
+ err = new Error();
254
+ err.name = msg.err.name;
255
+ err.message = msg.err.message;
256
+ err.stack = msg.err.stack;
257
+ }
258
+ pendingReply.reject(err);
259
+ }
260
+
261
+ private invokeHandler(proxyId: string, methodName: string, args: any[]): Promise<any> {
262
+ try {
263
+ return Promise.resolve(this.doInvokeHandler(proxyId, methodName, args));
264
+ } catch (err) {
265
+ return Promise.reject(err);
266
+ }
267
+ }
268
+
269
+ private doInvokeHandler(proxyId: string, methodName: string, args: any[]): any {
270
+ const actor = this.locals.get(proxyId);
271
+ if (!actor) {
272
+ throw new Error('Unknown actor ' + proxyId);
273
+ }
274
+ const method = actor[methodName];
275
+ if (typeof method !== 'function') {
276
+ throw new Error('Unknown method ' + methodName + ' on actor ' + proxyId);
277
+ }
278
+ return method.apply(actor, args);
279
+ }
280
+
281
+ private cancel(req: string): string {
282
+ return `{"type":${MessageType.Cancel},"id":"${req}"}`;
283
+ }
284
+
285
+ private open(req: string): string {
286
+ return `{"type":${MessageType.Open},"id":"${req}"}`;
287
+ }
288
+
289
+ private request(req: string, rpcId: string, method: string, args: any[]): string {
290
+ return `{"type":${MessageType.Request},"id":"${req}","proxyId":"${rpcId}","method":"${method}","args":${JSON.stringify(args, this.replacer)}}`;
291
+ }
292
+
293
+ private replyReady(req: string): string {
294
+ return `{"type":${MessageType.Ready},"id":"${req}"}`;
295
+
296
+ }
297
+
298
+ private replyOK(req: string, res: any): string {
299
+ if (typeof res === 'undefined') {
300
+ return `{"type":${MessageType.Reply},"id":"${req}"}`;
301
+ }
302
+ return `{"type":${MessageType.Reply},"id":"${req}","res":${safeStringify(
303
+ res,
304
+ this.replacer
305
+ )}}`;
306
+ }
307
+
308
+ private replyErr(req: string, err: any): string {
309
+ err = typeof err === 'string' ? new Error(err) : err;
310
+ if (err instanceof Error) {
311
+ return `{"type":${MessageType.ReplyErr},"id":"${req}","err":${safeStringify(
312
+ transformErrorForSerialization(err)
313
+ )}}`;
314
+ }
315
+ return `{"type":${MessageType.ReplyErr},"id":"${req}","err":null}`;
316
+ }
317
+ }
318
+
319
+ function canceled(): Error {
320
+ const error = new Error('Canceled');
321
+ error.name = error.message;
322
+ return error;
323
+ }
324
+
325
+ export function transformErrorForSerialization(error: Error): SerializedError {
326
+ if (error instanceof Error) {
327
+ const { name, message } = error;
328
+ const stack: string = (<any>error).stacktrace || error.stack;
329
+ return {
330
+ $isError: true,
331
+ name,
332
+ message,
333
+ stack,
334
+ };
335
+ }
336
+
337
+ // return as is
338
+ return error;
339
+ }
340
+
341
+ interface JSONStringifyReplacer {
342
+ (key: string, value: any): any;
343
+ }
344
+
345
+ function safeStringify(obj: any, replacer?: JSONStringifyReplacer): string {
346
+ try {
347
+ return JSON.stringify(obj, replacer);
348
+ } catch (err) {
349
+ console.error('error stringifying response: ', err);
350
+ return 'null';
351
+ }
352
+ }