hypha-rpc 0.1.0-post5

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.js ADDED
@@ -0,0 +1,1555 @@
1
+ /**
2
+ * Contains the RPC object used both by the application
3
+ * site, and by each plugin
4
+ */
5
+ import {
6
+ randId,
7
+ typedArrayToDtype,
8
+ dtypeToTypedArray,
9
+ MessageEmitter,
10
+ assert,
11
+ waitFor,
12
+ } from "./utils.js";
13
+
14
+ import { encode as msgpack_packb, decodeMulti } from "@msgpack/msgpack";
15
+
16
+ export const API_VERSION = "0.3.0";
17
+ const CHUNK_SIZE = 1024 * 500;
18
+
19
+ const ArrayBufferView = Object.getPrototypeOf(
20
+ Object.getPrototypeOf(new Uint8Array()),
21
+ ).constructor;
22
+
23
+ function _appendBuffer(buffer1, buffer2) {
24
+ const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
25
+ tmp.set(new Uint8Array(buffer1), 0);
26
+ tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
27
+ return tmp.buffer;
28
+ }
29
+
30
+ function indexObject(obj, is) {
31
+ if (!is) throw new Error("undefined index");
32
+ if (typeof is === "string") return indexObject(obj, is.split("."));
33
+ else if (is.length === 0) return obj;
34
+ else return indexObject(obj[is[0]], is.slice(1));
35
+ }
36
+ function getFunctionInfo(func) {
37
+ const funcString = func.toString();
38
+
39
+ // Extract function name
40
+ const nameMatch = funcString.match(/function\s*(\w*)/);
41
+ const name = (nameMatch && nameMatch[1]) || "";
42
+
43
+ // Extract function parameters, excluding comments
44
+ const paramsMatch = funcString.match(/\(([^)]*)\)/);
45
+ let params = "";
46
+ if (paramsMatch) {
47
+ params = paramsMatch[1]
48
+ .split(",")
49
+ .map((p) =>
50
+ p
51
+ .replace(/\/\*.*?\*\//g, "") // Remove block comments
52
+ .replace(/\/\/.*$/g, ""),
53
+ ) // Remove line comments
54
+ .filter((p) => p.trim().length > 0) // Remove empty strings after removing comments
55
+ .map((p) => p.trim()) // Trim remaining whitespace
56
+ .join(", ");
57
+ }
58
+
59
+ // Extract function docstring (block comment)
60
+ let docMatch = funcString.match(/\)\s*\{\s*\/\*([\s\S]*?)\*\//);
61
+ const docstringBlock = (docMatch && docMatch[1].trim()) || "";
62
+
63
+ // Extract function docstring (line comment)
64
+ docMatch = funcString.match(/\)\s*\{\s*(\/\/[\s\S]*?)\n\s*[^\s\/]/);
65
+ const docstringLine =
66
+ (docMatch &&
67
+ docMatch[1]
68
+ .split("\n")
69
+ .map((s) => s.replace(/^\/\/\s*/, "").trim())
70
+ .join("\n")) ||
71
+ "";
72
+
73
+ const docstring = docstringBlock || docstringLine;
74
+ return (
75
+ name &&
76
+ params.length > 0 && {
77
+ name: name,
78
+ sig: params,
79
+ doc: docstring,
80
+ }
81
+ );
82
+ }
83
+
84
+ function concatArrayBuffers(buffers) {
85
+ var buffersLengths = buffers.map(function (b) {
86
+ return b.byteLength;
87
+ }),
88
+ totalBufferlength = buffersLengths.reduce(function (p, c) {
89
+ return p + c;
90
+ }, 0),
91
+ unit8Arr = new Uint8Array(totalBufferlength);
92
+ buffersLengths.reduce(function (p, c, i) {
93
+ unit8Arr.set(new Uint8Array(buffers[i]), p);
94
+ return p + c;
95
+ }, 0);
96
+ return unit8Arr.buffer;
97
+ }
98
+
99
+ class Timer {
100
+ constructor(timeout, callback, args, label) {
101
+ this._timeout = timeout;
102
+ this._callback = callback;
103
+ this._args = args;
104
+ this._label = label || "timer";
105
+ this._task = null;
106
+ this.started = false;
107
+ }
108
+
109
+ start() {
110
+ if (this.started) {
111
+ this.reset();
112
+ } else {
113
+ this._task = setTimeout(() => {
114
+ this._callback.apply(this, this._args);
115
+ }, this._timeout * 1000);
116
+ this.started = true;
117
+ }
118
+ }
119
+
120
+ clear() {
121
+ if (this._task) {
122
+ clearTimeout(this._task);
123
+ this._task = null;
124
+ this.started = false;
125
+ } else {
126
+ console.warn(`Clearing a timer (${this._label}) which is not started`);
127
+ }
128
+ }
129
+
130
+ reset() {
131
+ if (this._task) {
132
+ clearTimeout(this._task);
133
+ }
134
+ this._task = setTimeout(() => {
135
+ this._callback.apply(this, this._args);
136
+ }, this._timeout * 1000);
137
+ this.started = true;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * RPC object represents a single site in the
143
+ * communication protocol between the application and the plugin
144
+ *
145
+ * @param {Object} connection a special object allowing to send
146
+ * and receive messages from the opposite site (basically it
147
+ * should only provide send() and onMessage() methods)
148
+ */
149
+ export class RPC extends MessageEmitter {
150
+ constructor(
151
+ connection,
152
+ {
153
+ client_id = null,
154
+ manager_id = null,
155
+ default_context = null,
156
+ name = null,
157
+ codecs = null,
158
+ method_timeout = null,
159
+ max_message_buffer_size = 0,
160
+ debug = false,
161
+ workspace = null,
162
+ silent = false,
163
+ app_id = null,
164
+ },
165
+ ) {
166
+ super(debug);
167
+ this._codecs = codecs || {};
168
+ assert(client_id && typeof client_id === "string");
169
+ assert(client_id, "client_id is required");
170
+ this._client_id = client_id;
171
+ this._name = name;
172
+ this._app_id = app_id || "*";
173
+ this._local_workspace = workspace;
174
+ this.manager_id = manager_id;
175
+ this._silent = silent;
176
+ this.default_context = default_context || {};
177
+ this._method_annotations = new WeakMap();
178
+ this._max_message_buffer_size = max_message_buffer_size;
179
+ this._chunk_store = {};
180
+ this._method_timeout = method_timeout || 30;
181
+
182
+ // make sure there is an execute function
183
+ this._services = {};
184
+ this._object_store = {
185
+ services: this._services,
186
+ };
187
+
188
+ if (connection) {
189
+ this.add_service({
190
+ id: "built-in",
191
+ type: "built-in",
192
+ name: `Built-in services for ${this._local_workspace}/${this._client_id}`,
193
+ config: { require_context: true, visibility: "public" },
194
+ ping: this._ping.bind(this),
195
+ get_service: this.get_local_service.bind(this),
196
+ register_service: this.register_service.bind(this),
197
+ message_cache: {
198
+ create: this._create_message.bind(this),
199
+ append: this._append_message.bind(this),
200
+ process: this._process_message.bind(this),
201
+ remove: this._remove_message.bind(this),
202
+ },
203
+ });
204
+ this.on("method", this._handle_method.bind(this));
205
+
206
+ assert(connection.emit_message && connection.on_message);
207
+ this._emit_message = connection.emit_message.bind(connection);
208
+ connection.on_message(this._on_message.bind(this));
209
+ this._connection = connection;
210
+ const updateServices = async () => {
211
+ if (!this._silent && this.manager_id) {
212
+ console.log("Connection established, reporting services...");
213
+ for (let service of Object.values(this._services)) {
214
+ const serviceInfo = this._extract_service_info(service);
215
+ await this.emit({
216
+ type: "service-added",
217
+ to: "*/" + this.manager_id,
218
+ service: serviceInfo,
219
+ });
220
+ }
221
+ }
222
+ }
223
+ connection.on_connect(updateServices);
224
+ updateServices();
225
+ } else {
226
+ this._emit_message = function () {
227
+ console.log("No connection to emit message");
228
+ };
229
+ }
230
+ }
231
+
232
+ register_codec(config) {
233
+ if (!config["name"] || (!config["encoder"] && !config["decoder"])) {
234
+ throw new Error(
235
+ "Invalid codec format, please make sure you provide a name, type, encoder and decoder.",
236
+ );
237
+ } else {
238
+ if (config.type) {
239
+ for (let k of Object.keys(this._codecs)) {
240
+ if (this._codecs[k].type === config.type || k === config.name) {
241
+ delete this._codecs[k];
242
+ console.warn("Remove duplicated codec: " + k);
243
+ }
244
+ }
245
+ }
246
+ this._codecs[config["name"]] = config;
247
+ }
248
+ }
249
+
250
+ async _ping(msg, context) {
251
+ assert(msg == "ping");
252
+ return "pong";
253
+ }
254
+
255
+ async ping(client_id, timeout) {
256
+ let method = this._generate_remote_method({
257
+ _rtarget: client_id,
258
+ _rmethod: "services.built-in.ping",
259
+ _rpromise: true,
260
+ _rdoc: "Ping a remote client",
261
+ _rsig: "ping(msg)",
262
+ });
263
+ assert((await method("ping", timeout)) == "pong");
264
+ }
265
+
266
+ _create_message(key, heartbeat, overwrite, context) {
267
+ if (heartbeat) {
268
+ if (!this._object_store[key]) {
269
+ throw new Error(`session does not exist anymore: ${key}`);
270
+ }
271
+ this._object_store[key]["timer"].reset();
272
+ }
273
+
274
+ if (!this._object_store["message_cache"]) {
275
+ this._object_store["message_cache"] = {};
276
+ }
277
+ if (!overwrite && this._object_store["message_cache"][key]) {
278
+ throw new Error(
279
+ `Message with the same key (${key}) already exists in the cache store, please use overwrite=true or remove it first.`,
280
+ );
281
+ }
282
+
283
+ this._object_store["message_cache"][key] = [];
284
+ }
285
+
286
+ _append_message(key, data, heartbeat, context) {
287
+ if (heartbeat) {
288
+ if (!this._object_store[key]) {
289
+ throw new Error(`session does not exist anymore: ${key}`);
290
+ }
291
+ this._object_store[key]["timer"].reset();
292
+ }
293
+ const cache = this._object_store["message_cache"];
294
+ if (!cache[key]) {
295
+ throw new Error(`Message with key ${key} does not exists.`);
296
+ }
297
+ assert(data instanceof ArrayBufferView);
298
+ cache[key].push(data);
299
+ }
300
+
301
+ _remove_message(key, context) {
302
+ const cache = this._object_store["message_cache"];
303
+ if (!cache[key]) {
304
+ throw new Error(`Message with key ${key} does not exists.`);
305
+ }
306
+ delete cache[key];
307
+ }
308
+
309
+ _process_message(key, heartbeat, context) {
310
+ if (heartbeat) {
311
+ if (!this._object_store[key]) {
312
+ throw new Error(`session does not exist anymore: ${key}`);
313
+ }
314
+ this._object_store[key]["timer"].reset();
315
+ }
316
+ const cache = this._object_store["message_cache"];
317
+ assert(!!context, "Context is required");
318
+ if (!cache[key]) {
319
+ throw new Error(`Message with key ${key} does not exists.`);
320
+ }
321
+ cache[key] = concatArrayBuffers(cache[key]);
322
+ console.debug(`Processing message ${key} (bytes=${cache[key].byteLength})`);
323
+ let unpacker = decodeMulti(cache[key]);
324
+ const { done, value } = unpacker.next();
325
+ const main = value;
326
+ // Make sure the fields are from trusted source
327
+ Object.assign(main, {
328
+ from: context.from,
329
+ to: context.to,
330
+ user: context.user,
331
+ });
332
+ main["ctx"] = JSON.parse(JSON.stringify(main));
333
+ Object.assign(main["ctx"], this.default_context);
334
+ if (!done) {
335
+ let extra = unpacker.next();
336
+ Object.assign(main, extra.value);
337
+ }
338
+ this._fire(main["type"], main);
339
+ console.debug(
340
+ this._client_id,
341
+ `Processed message ${key} (bytes=${cache[key].byteLength})`,
342
+ );
343
+ delete cache[key];
344
+ }
345
+
346
+ _on_message(message) {
347
+ try {
348
+ assert(message instanceof ArrayBuffer);
349
+ let unpacker = decodeMulti(message);
350
+ const { done, value } = unpacker.next();
351
+ const main = value;
352
+ // Add trusted context to the method call
353
+ main["ctx"] = JSON.parse(JSON.stringify(main));
354
+ Object.assign(main["ctx"], this.default_context);
355
+ if (!done) {
356
+ let extra = unpacker.next();
357
+ Object.assign(main, extra.value);
358
+ }
359
+ this._fire(main["type"], main);
360
+ } catch (error) {
361
+ console.error("Failed to process message", error);
362
+ }
363
+ }
364
+
365
+ reset() {
366
+ this._event_handlers = {};
367
+ this._services = {};
368
+ }
369
+
370
+ async disconnect() {
371
+ this._fire("disconnect");
372
+ await this._connection.disconnect();
373
+ }
374
+
375
+ async get_manager_service(timeout) {
376
+ assert(this.manager_id, "Manager id is not set");
377
+ const svc = await this.get_remote_service(
378
+ `*/${this.manager_id}:default`,
379
+ timeout,
380
+ );
381
+ return svc;
382
+ }
383
+
384
+ get_all_local_services() {
385
+ return this._services;
386
+ }
387
+ get_local_service(service_id, context) {
388
+ assert(service_id);
389
+ const [ws, client_id] = context["to"].split("/");
390
+ assert(
391
+ client_id === this._client_id,
392
+ "Services can only be accessed locally",
393
+ );
394
+
395
+ const service = this._services[service_id];
396
+ if (!service) {
397
+ throw new Error("Service not found: " + service_id);
398
+ }
399
+
400
+ service.config["workspace"] = ws;
401
+ // allow access for the same workspace
402
+ if (service.config.visibility == "public") {
403
+ return service;
404
+ }
405
+
406
+ // allow access for the same workspace
407
+ if (context["ws"] === ws) {
408
+ return service;
409
+ }
410
+
411
+ throw new Error(
412
+ `Permission denied for protected service: ${service_id}, workspace mismatch: ${ws} != ${context["ws"]}`,
413
+ );
414
+ }
415
+ async get_remote_service(service_uri, timeout) {
416
+ timeout = timeout === undefined ? this._method_timeout : timeout;
417
+ if (!service_uri && this.manager_id) {
418
+ service_uri = "*/" + this.manager_id;
419
+ } else if (!service_uri.includes(":")) {
420
+ service_uri = this._client_id + ":" + service_uri;
421
+ }
422
+ const provider = service_uri.split(":")[0];
423
+ let service_id = service_uri.split(":")[1];
424
+ if (service_id.includes("@")) {
425
+ service_id = service_id.split("@")[0];
426
+ const app_id = service_uri.split("@")[1];
427
+ if (this._app_id)
428
+ assert(
429
+ app_id === this._app_id,
430
+ `Invalid app id: ${app_id} != ${this._app_id}`,
431
+ );
432
+ }
433
+ assert(provider, `Invalid service uri: ${service_uri}`);
434
+
435
+ try {
436
+ const method = this._generate_remote_method({
437
+ _rtarget: provider,
438
+ _rmethod: "services.built-in.get_service",
439
+ _rpromise: true,
440
+ _rdoc: "Get a remote service",
441
+ _rsig: "get_service(service_id)",
442
+ });
443
+ const svc = await waitFor(
444
+ method(service_id),
445
+ timeout,
446
+ "Timeout Error: Failed to get remote service: " + service_uri,
447
+ );
448
+ svc.id = `${provider}:${service_id}`;
449
+ return svc;
450
+ } catch (e) {
451
+ console.error("Failed to get remote service: " + service_uri, e);
452
+ throw e;
453
+ }
454
+ }
455
+ _annotate_service_methods(
456
+ aObject,
457
+ object_id,
458
+ require_context,
459
+ run_in_executor,
460
+ visibility,
461
+ ) {
462
+ if (typeof aObject === "function") {
463
+ // mark the method as a remote method that requires context
464
+ let method_name = object_id.split(".")[1];
465
+ this._method_annotations.set(aObject, {
466
+ require_context: Array.isArray(require_context)
467
+ ? require_context.includes(method_name)
468
+ : !!require_context,
469
+ run_in_executor: run_in_executor,
470
+ method_id: "services." + object_id,
471
+ visibility: visibility,
472
+ });
473
+ } else if (aObject instanceof Array || aObject instanceof Object) {
474
+ for (let key of Object.keys(aObject)) {
475
+ let val = aObject[key];
476
+ if (typeof val === "function" && val.__rpc_object__) {
477
+ let client_id = val.__rpc_object__._rtarget;
478
+ if (client_id.includes("/")) {
479
+ client_id = client_id.split("/")[1];
480
+ }
481
+ if (this._client_id === client_id) {
482
+ if (aObject instanceof Array) {
483
+ aObject = aObject.slice();
484
+ }
485
+ // recover local method
486
+ aObject[key] = indexObject(
487
+ this._object_store,
488
+ val.__rpc_object__._rmethod,
489
+ );
490
+ val = aObject[key]; // make sure it's annotated later
491
+ } else {
492
+ throw new Error(
493
+ `Local method not found: ${val.__rpc_object__._rmethod}, client id mismatch ${this._client_id} != ${client_id}`,
494
+ );
495
+ }
496
+ }
497
+ this._annotate_service_methods(
498
+ val,
499
+ object_id + "." + key,
500
+ require_context,
501
+ run_in_executor,
502
+ visibility,
503
+ );
504
+ }
505
+ }
506
+ }
507
+ add_service(api, overwrite) {
508
+ if (!api || Array.isArray(api)) throw new Error("Invalid service object");
509
+ if (api.constructor === Object) {
510
+ api = Object.assign({}, api);
511
+ } else {
512
+ const normApi = {};
513
+ const props = Object.getOwnPropertyNames(api).concat(
514
+ Object.getOwnPropertyNames(Object.getPrototypeOf(api)),
515
+ );
516
+ for (let k of props) {
517
+ if (k !== "constructor") {
518
+ if (typeof api[k] === "function") normApi[k] = api[k].bind(api);
519
+ else normApi[k] = api[k];
520
+ }
521
+ }
522
+ // For class instance, we need set a default id
523
+ api.id = api.id || "default";
524
+ api = normApi;
525
+ }
526
+ assert(
527
+ api.id && typeof api.id === "string",
528
+ `Service id not found: ${api}`,
529
+ );
530
+ if (!api.name) {
531
+ api.name = api.id;
532
+ }
533
+ if (!api.config) {
534
+ api.config = {};
535
+ }
536
+ if (!api.type) {
537
+ api.type = "generic";
538
+ }
539
+ // require_context only applies to the top-level functions
540
+ let require_context = false,
541
+ run_in_executor = false;
542
+ if (api.config.require_context)
543
+ require_context = api.config.require_context;
544
+ if (api.config.run_in_executor) run_in_executor = true;
545
+ const visibility = api.config.visibility || "protected";
546
+ assert(["protected", "public"].includes(visibility));
547
+ this._annotate_service_methods(
548
+ api,
549
+ api["id"],
550
+ require_context,
551
+ run_in_executor,
552
+ visibility,
553
+ );
554
+
555
+ if (this._services[api.id]) {
556
+ if (overwrite) {
557
+ delete this._services[api.id];
558
+ } else {
559
+ throw new Error(
560
+ `Service already exists: ${api.id}, please specify a different id (not ${api.id}) or overwrite=true`,
561
+ );
562
+ }
563
+ }
564
+ this._services[api.id] = api;
565
+ return api;
566
+ }
567
+
568
+ _extract_service_info(service) {
569
+ return {
570
+ id: `${this._client_id}:${service["id"]}`,
571
+ type: service["type"],
572
+ name: service["name"],
573
+ description: service["description"] || "",
574
+ config: service["config"],
575
+ app_id: this._app_id,
576
+ };
577
+ }
578
+
579
+ async register_service(api, overwrite, notify, context) {
580
+ if (notify === undefined) notify = true;
581
+ if (context) {
582
+ // If this function is called from remote, we need to make sure
583
+ const [workspace, client_id] = context["to"].split("/");
584
+ assert(client_id === this._client_id);
585
+ assert(
586
+ workspace === context["ws"],
587
+ "Services can only be registered from the same workspace",
588
+ );
589
+ }
590
+ const service = this.add_service(api, overwrite);
591
+ const serviceInfo = this._extract_service_info(service);
592
+ if (notify) {
593
+ if (this.manager_id) {
594
+ this.emit({
595
+ type: "service-added",
596
+ to: "*/" + this.manager_id,
597
+ service: serviceInfo,
598
+ });
599
+ } else {
600
+ this.emit({ type: "service-added", to: "*", service: serviceInfo });
601
+ }
602
+ }
603
+ return serviceInfo;
604
+ }
605
+ async unregister_service(service, notify) {
606
+ if (service instanceof Object) {
607
+ service = service.id;
608
+ }
609
+ if (!this._services[service]) {
610
+ throw new Error(`Service not found: ${service}`);
611
+ }
612
+ const api = this._services[service];
613
+ delete this._services[service];
614
+ if (notify) {
615
+ const serviceInfo = this._extract_service_info(api);
616
+ if (this.manager_id) {
617
+ this.emit({
618
+ type: "service-removed",
619
+ to: "*/" + this.manager_id,
620
+ service: serviceInfo,
621
+ });
622
+ } else {
623
+ this.emit({ type: "service-removed", to: "*", service: serviceInfo });
624
+ }
625
+ }
626
+ }
627
+
628
+ _ndarray(typedArray, shape, dtype) {
629
+ const _dtype = typedArrayToDtype(typedArray);
630
+ if (dtype && dtype !== _dtype) {
631
+ throw (
632
+ "dtype doesn't match the type of the array: " + _dtype + " != " + dtype
633
+ );
634
+ }
635
+ shape = shape || [typedArray.length];
636
+ return {
637
+ _rtype: "ndarray",
638
+ _rvalue: typedArray.buffer,
639
+ _rshape: shape,
640
+ _rdtype: _dtype,
641
+ };
642
+ }
643
+
644
+ _encode_callback(
645
+ name,
646
+ callback,
647
+ session_id,
648
+ clear_after_called,
649
+ timer,
650
+ local_workspace,
651
+ ) {
652
+ let method_id = `${session_id}.${name}`;
653
+ let encoded = {
654
+ _rtype: "method",
655
+ _rtarget: local_workspace
656
+ ? `${local_workspace}/${this._client_id}`
657
+ : this._client_id,
658
+ _rmethod: method_id,
659
+ _rpromise: false,
660
+ };
661
+
662
+ const self = this;
663
+ let wrapped_callback = function () {
664
+ try {
665
+ callback.apply(null, Array.prototype.slice.call(arguments));
666
+ } catch (error) {
667
+ console.error("Error in callback:", method_id, error);
668
+ } finally {
669
+ if (clear_after_called && self._object_store[session_id]) {
670
+ // console.log("Deleting session", session_id, "from", self._client_id);
671
+ delete self._object_store[session_id];
672
+ }
673
+ if (timer && timer.started) {
674
+ timer.clear();
675
+ }
676
+ }
677
+ };
678
+
679
+ return [encoded, wrapped_callback];
680
+ }
681
+
682
+ async _encode_promise(
683
+ resolve,
684
+ reject,
685
+ session_id,
686
+ clear_after_called,
687
+ timer,
688
+ local_workspace,
689
+ ) {
690
+ let store = this._get_session_store(session_id, true);
691
+ assert(
692
+ store,
693
+ `Failed to create session store ${session_id} due to invalid parent`,
694
+ );
695
+ let encoded = {};
696
+
697
+ if (timer && reject && this._method_timeout) {
698
+ encoded.heartbeat = await this._encode(
699
+ timer.reset.bind(timer),
700
+ session_id,
701
+ local_workspace,
702
+ );
703
+ encoded.interval = this._method_timeout / 2;
704
+ store.timer = timer;
705
+ } else {
706
+ timer = null;
707
+ }
708
+
709
+ [encoded.resolve, store.resolve] = this._encode_callback(
710
+ "resolve",
711
+ resolve,
712
+ session_id,
713
+ clear_after_called,
714
+ timer,
715
+ local_workspace,
716
+ );
717
+ [encoded.reject, store.reject] = this._encode_callback(
718
+ "reject",
719
+ reject,
720
+ session_id,
721
+ clear_after_called,
722
+ timer,
723
+ local_workspace,
724
+ );
725
+ return encoded;
726
+ }
727
+
728
+ async _send_chunks(data, target_id, session_id) {
729
+ let remote_services = await this.get_remote_service(
730
+ `${target_id}:built-in`,
731
+ );
732
+ assert(
733
+ remote_services.message_cache,
734
+ "Remote client does not support message caching for long message.",
735
+ );
736
+ let message_cache = remote_services.message_cache;
737
+ let message_id = session_id || randId();
738
+ await message_cache.create(message_id, !!session_id);
739
+ let total_size = data.length;
740
+ let chunk_num = Math.ceil(total_size / CHUNK_SIZE);
741
+ for (let idx = 0; idx < chunk_num; idx++) {
742
+ let start_byte = idx * CHUNK_SIZE;
743
+ await message_cache.append(
744
+ message_id,
745
+ data.slice(start_byte, start_byte + CHUNK_SIZE),
746
+ !!session_id,
747
+ );
748
+ console.log(
749
+ `Sending chunk ${idx + 1}/${chunk_num} (${total_size} bytes)`,
750
+ );
751
+ }
752
+ // console.log(`All chunks sent (${chunk_num})`);
753
+ await message_cache.process(message_id, !!session_id);
754
+ }
755
+
756
+ emit(main_message, extra_data) {
757
+ assert(
758
+ typeof main_message === "object" && main_message.type,
759
+ "Invalid message, must be an object with a type field.",
760
+ );
761
+ let message_package = msgpack_packb(main_message);
762
+ if (extra_data) {
763
+ const extra = msgpack_packb(extra_data);
764
+ message_package = new Uint8Array([...message_package, ...extra]);
765
+ }
766
+ const total_size = message_package.length;
767
+ if (total_size <= CHUNK_SIZE + 1024) {
768
+ return this._emit_message(message_package);
769
+ } else {
770
+ throw new Error("Message is too large to send in one go.");
771
+ }
772
+ }
773
+
774
+ _generate_remote_method(
775
+ encoded_method,
776
+ remote_parent,
777
+ local_parent,
778
+ remote_workspace,
779
+ local_workspace,
780
+ ) {
781
+ let target_id = encoded_method._rtarget;
782
+ if (remote_workspace && !target_id.includes("/")) {
783
+ if (remote_workspace !== target_id) {
784
+ target_id = remote_workspace + "/" + target_id;
785
+ }
786
+ // Fix the target id to be an absolute id
787
+ encoded_method._rtarget = target_id;
788
+ }
789
+ let method_id = encoded_method._rmethod;
790
+ let with_promise = encoded_method._rpromise;
791
+ const self = this;
792
+
793
+ function remote_method() {
794
+ return new Promise(async (resolve, reject) => {
795
+ let local_session_id = randId();
796
+ if (local_parent) {
797
+ // Store the children session under the parent
798
+ local_session_id = local_parent + "." + local_session_id;
799
+ }
800
+ let store = self._get_session_store(local_session_id, true);
801
+ if (!store) {
802
+ reject(
803
+ new Error(
804
+ `Runtime Error: Failed to get session store ${local_session_id}`,
805
+ ),
806
+ );
807
+ return;
808
+ }
809
+ store["target_id"] = target_id;
810
+ const args = await self._encode(
811
+ Array.prototype.slice.call(arguments),
812
+ local_session_id,
813
+ local_workspace,
814
+ );
815
+ const argLength = args.length;
816
+ // if the last argument is an object, mark it as kwargs
817
+ const withKwargs =
818
+ argLength > 0 &&
819
+ typeof args[argLength - 1] === "object" &&
820
+ args[argLength - 1] !== null &&
821
+ args[argLength - 1]._rkwargs;
822
+ if (withKwargs) delete args[argLength - 1]._rkwargs;
823
+
824
+ let from_client;
825
+ if (!self._local_workspace) {
826
+ from_client = self._client_id;
827
+ } else {
828
+ from_client = self._local_workspace + "/" + self._client_id;
829
+ }
830
+
831
+ let main_message = {
832
+ type: "method",
833
+ from: from_client,
834
+ to: target_id,
835
+ method: method_id,
836
+ };
837
+ let extra_data = {};
838
+ if (args) {
839
+ extra_data["args"] = args;
840
+ }
841
+ if (withKwargs) {
842
+ extra_data["with_kwargs"] = withKwargs;
843
+ }
844
+
845
+ // console.log(
846
+ // `Calling remote method ${target_id}:${method_id}, session: ${local_session_id}`
847
+ // );
848
+ if (remote_parent) {
849
+ // Set the parent session
850
+ // Note: It's a session id for the remote, not the current client
851
+ main_message["parent"] = remote_parent;
852
+ }
853
+
854
+ let timer = null;
855
+ if (with_promise) {
856
+ // Only pass the current session id to the remote
857
+ // if we want to received the result
858
+ // I.e. the session id won't be passed for promises themselves
859
+ main_message["session"] = local_session_id;
860
+ let method_name = `${target_id}:${method_id}`;
861
+ timer = new Timer(
862
+ self._method_timeout,
863
+ reject,
864
+ [`Method call time out: ${method_name}`],
865
+ method_name,
866
+ );
867
+ // By default, hypha will clear the session after the method is called
868
+ // However, if the args contains _rintf === true, we will not clear the session
869
+ let clear_after_called = true;
870
+ for (let arg of args) {
871
+ if (typeof arg === "object" && arg._rintf === true) {
872
+ clear_after_called = false;
873
+ break;
874
+ }
875
+ }
876
+ extra_data["promise"] = await self._encode_promise(
877
+ resolve,
878
+ reject,
879
+ local_session_id,
880
+ clear_after_called,
881
+ timer,
882
+ local_workspace,
883
+ );
884
+ }
885
+ // The message consists of two segments, the main message and extra data
886
+ let message_package = msgpack_packb(main_message);
887
+ if (extra_data) {
888
+ const extra = msgpack_packb(extra_data);
889
+ message_package = new Uint8Array([...message_package, ...extra]);
890
+ }
891
+ const total_size = message_package.length;
892
+ if (total_size <= CHUNK_SIZE + 1024) {
893
+ self._emit_message(message_package).then(function () {
894
+ if (timer) {
895
+ // console.log(`Start watchdog timer.`);
896
+ // Only start the timer after we send the message successfully
897
+ timer.start();
898
+ }
899
+ });
900
+ } else {
901
+ // send chunk by chunk
902
+ self
903
+ ._send_chunks(message_package, target_id, remote_parent)
904
+ .then(function () {
905
+ if (timer) {
906
+ // console.log(`Start watchdog timer.`);
907
+ // Only start the timer after we send the message successfully
908
+ timer.start();
909
+ }
910
+ })
911
+ .catch(function (err) {
912
+ console.error("Failed to send message", err);
913
+ reject(err);
914
+ });
915
+ }
916
+ });
917
+ }
918
+
919
+ // Generate debugging information for the method
920
+ remote_method.__rpc_object__ = encoded_method;
921
+ const parts = method_id.split(".");
922
+ remote_method.__name__ = parts[parts.length - 1];
923
+ remote_method.__doc__ = encoded_method._rdoc;
924
+ remote_method.__sig__ = encoded_method._rsig;
925
+ return remote_method;
926
+ }
927
+
928
+ get_client_info() {
929
+ const services = [];
930
+ for (let service of Object.values(this._services)) {
931
+ services.push(this._extract_service_info(service));
932
+ }
933
+
934
+ return {
935
+ id: this._client_id,
936
+ services: services,
937
+ };
938
+ }
939
+
940
+ async _handle_method(data) {
941
+ let reject = null;
942
+ let heartbeat_task = null;
943
+ try {
944
+ assert(data.method && data.ctx && data.from && data.ws);
945
+ const method_name = data.from + ":" + data.method;
946
+ const remote_workspace = data.from.split("/")[0];
947
+ // Make sure the target id is an absolute id
948
+ data["to"] = data["to"].includes("/")
949
+ ? data["to"]
950
+ : remote_workspace + "/" + data["to"];
951
+ data["ctx"]["to"] = data["to"];
952
+ let local_workspace;
953
+ if (!this._local_workspace) {
954
+ local_workspace = data["to"].split("/")[0];
955
+ } else {
956
+ if (this._local_workspace && this._local_workspace !== "*") {
957
+ assert(
958
+ data["to"].split("/")[0] === this._local_workspace,
959
+ "Workspace mismatch: " +
960
+ data["to"].split("/")[0] +
961
+ " != " +
962
+ this._local_workspace,
963
+ );
964
+ }
965
+ local_workspace = this._local_workspace;
966
+ }
967
+ const local_parent = data.parent;
968
+
969
+ let resolve, reject;
970
+ if (data.promise) {
971
+ // Decode the promise with the remote session id
972
+ // Such that the session id will be passed to the remote as a parent session id
973
+ const promise = await this._decode(
974
+ data.promise,
975
+ data.session,
976
+ local_parent,
977
+ remote_workspace,
978
+ local_workspace,
979
+ );
980
+ resolve = promise.resolve;
981
+ reject = promise.reject;
982
+ if (promise.heartbeat && promise.interval) {
983
+ async function heartbeat() {
984
+ try {
985
+ console.log("Reset heartbeat timer: " + data.method);
986
+ await promise.heartbeat();
987
+ } catch (err) {
988
+ console.error(err);
989
+ }
990
+ }
991
+ heartbeat_task = setInterval(heartbeat, promise.interval * 1000);
992
+ }
993
+ }
994
+
995
+ let method;
996
+
997
+ try {
998
+ method = indexObject(this._object_store, data["method"]);
999
+ } catch (e) {
1000
+ console.debug("Failed to find method", method_name, this._client_id, e);
1001
+ throw new Error(`Method not found: ${method_name} at ${this._client_id}`);
1002
+ }
1003
+
1004
+ assert(
1005
+ method && typeof method === "function",
1006
+ "Invalid method: " + method_name,
1007
+ );
1008
+
1009
+ // Check permission
1010
+ if (this._method_annotations.has(method)) {
1011
+ // For services, it should not be protected
1012
+ if (this._method_annotations.get(method).visibility === "protected") {
1013
+ if (local_workspace !== remote_workspace) {
1014
+ throw new Error(
1015
+ "Permission denied for protected method " +
1016
+ method_name +
1017
+ ", workspace mismatch: " +
1018
+ local_workspace +
1019
+ " != " +
1020
+ remote_workspace,
1021
+ );
1022
+ }
1023
+ }
1024
+ } else {
1025
+ // For sessions, the target_id should match exactly
1026
+ let session_target_id =
1027
+ this._object_store[data.method.split(".")[0]].target_id;
1028
+ if (
1029
+ local_workspace === remote_workspace &&
1030
+ session_target_id &&
1031
+ session_target_id.indexOf("/") === -1
1032
+ ) {
1033
+ session_target_id = local_workspace + "/" + session_target_id;
1034
+ }
1035
+ if (session_target_id !== data.from) {
1036
+ throw new Error(
1037
+ "Access denied for method call (" +
1038
+ method_name +
1039
+ ") from " +
1040
+ data.from +
1041
+ " to target " +
1042
+ session_target_id,
1043
+ );
1044
+ }
1045
+ }
1046
+
1047
+ // Make sure the parent session is still open
1048
+ if (local_parent) {
1049
+ // The parent session should be a session that generate the current method call
1050
+ assert(
1051
+ this._get_session_store(local_parent, true) !== null,
1052
+ "Parent session was closed: " + local_parent,
1053
+ );
1054
+ }
1055
+ let args;
1056
+ if (data.args) {
1057
+ args = await this._decode(
1058
+ data.args,
1059
+ data.session,
1060
+ null,
1061
+ remote_workspace,
1062
+ null,
1063
+ );
1064
+ } else {
1065
+ args = [];
1066
+ }
1067
+ if (
1068
+ this._method_annotations.has(method) &&
1069
+ this._method_annotations.get(method).require_context
1070
+ ) {
1071
+ args.push(data.ctx);
1072
+ }
1073
+ // console.log("Executing method: " + method_name);
1074
+ if (data.promise) {
1075
+ const result = method.apply(null, args);
1076
+ if (result instanceof Promise) {
1077
+ result
1078
+ .then((result) => {
1079
+ resolve(result);
1080
+ clearInterval(heartbeat_task);
1081
+ })
1082
+ .catch((err) => {
1083
+ reject(err);
1084
+ clearInterval(heartbeat_task);
1085
+ });
1086
+ } else {
1087
+ resolve(result);
1088
+ clearInterval(heartbeat_task);
1089
+ }
1090
+ } else {
1091
+ method.apply(null, args);
1092
+ clearInterval(heartbeat_task);
1093
+ }
1094
+ } catch (err) {
1095
+ if (reject) {
1096
+ reject(err);
1097
+ console.debug("Error during calling method: ", err);
1098
+ } else {
1099
+ console.error("Error during calling method: ", err);
1100
+ }
1101
+ // make sure we clear the heartbeat timer
1102
+ clearInterval(heartbeat_task);
1103
+ }
1104
+ }
1105
+
1106
+ encode(aObject, session_id) {
1107
+ return this._encode(aObject, session_id);
1108
+ }
1109
+
1110
+ _get_session_store(session_id, create) {
1111
+ let store = this._object_store;
1112
+ const levels = session_id.split(".");
1113
+ if (create) {
1114
+ const last_index = levels.length - 1;
1115
+ for (let level of levels.slice(0, last_index)) {
1116
+ if (!store[level]) {
1117
+ return null;
1118
+ }
1119
+ store = store[level];
1120
+ }
1121
+ // Create the last level
1122
+ if (!store[levels[last_index]]) {
1123
+ store[levels[last_index]] = {};
1124
+ }
1125
+ return store[levels[last_index]];
1126
+ } else {
1127
+ for (let level of levels) {
1128
+ if (!store[level]) {
1129
+ return null;
1130
+ }
1131
+ store = store[level];
1132
+ }
1133
+ return store;
1134
+ }
1135
+ }
1136
+
1137
+ /**
1138
+ * Prepares the provided set of remote method arguments for
1139
+ * sending to the remote site, replaces all the callbacks with
1140
+ * identifiers
1141
+ *
1142
+ * @param {Array} args to wrap
1143
+ *
1144
+ * @returns {Array} wrapped arguments
1145
+ */
1146
+ async _encode(aObject, session_id, local_workspace) {
1147
+ const aType = typeof aObject;
1148
+ if (
1149
+ aType === "number" ||
1150
+ aType === "string" ||
1151
+ aType === "boolean" ||
1152
+ aObject === null ||
1153
+ aObject === undefined ||
1154
+ aObject instanceof Uint8Array
1155
+ ) {
1156
+ return aObject;
1157
+ }
1158
+ if (aObject instanceof ArrayBuffer) {
1159
+ return {
1160
+ _rtype: "memoryview",
1161
+ _rvalue: new Uint8Array(aObject),
1162
+ };
1163
+ }
1164
+ // Reuse the remote object
1165
+ if (aObject.__rpc_object__) {
1166
+ return aObject.__rpc_object__;
1167
+ }
1168
+
1169
+ let bObject;
1170
+
1171
+ // skip if already encoded
1172
+ if (aObject.constructor instanceof Object && aObject._rtype) {
1173
+ // make sure the interface functions are encoded
1174
+ const temp = aObject._rtype;
1175
+ delete aObject._rtype;
1176
+ bObject = await this._encode(aObject, session_id, local_workspace);
1177
+ bObject._rtype = temp;
1178
+ return bObject;
1179
+ }
1180
+
1181
+ if (typeof aObject === "function") {
1182
+ if (this._method_annotations.has(aObject)) {
1183
+ let annotation = this._method_annotations.get(aObject);
1184
+ bObject = {
1185
+ _rtype: "method",
1186
+ _rtarget: this._client_id,
1187
+ _rmethod: annotation.method_id,
1188
+ _rpromise: true,
1189
+ };
1190
+ } else {
1191
+ assert(typeof session_id === "string");
1192
+ let object_id;
1193
+ if (aObject.__name__) {
1194
+ object_id = `${randId()}-${aObject.__name__}`;
1195
+ } else {
1196
+ object_id = randId();
1197
+ }
1198
+ bObject = {
1199
+ _rtype: "method",
1200
+ _rtarget: this._client_id,
1201
+ _rmethod: `${session_id}.${object_id}`,
1202
+ _rpromise: true,
1203
+ };
1204
+ let store = this._get_session_store(session_id, true);
1205
+ assert(
1206
+ store !== null,
1207
+ `Failed to create session store ${session_id} due to invalid parent`,
1208
+ );
1209
+ store[object_id] = aObject;
1210
+ }
1211
+ bObject._rdoc = aObject.__doc__;
1212
+ bObject._rsig = aObject.__sig__;
1213
+ if (!bObject._rdoc || !bObject._rsig) {
1214
+ try {
1215
+ const funcInfo = getFunctionInfo(aObject);
1216
+ if (funcInfo && !bObject._rdoc) {
1217
+ bObject._rdoc = `${funcInfo.doc}`;
1218
+ }
1219
+ if (funcInfo && !bObject._rsig) {
1220
+ bObject._rsig = `${funcInfo.name}(${funcInfo.sig})`;
1221
+ }
1222
+ } catch (e) {
1223
+ console.error("Failed to extract function docstring:", aObject);
1224
+ }
1225
+ }
1226
+
1227
+ return bObject;
1228
+ }
1229
+ const isarray = Array.isArray(aObject);
1230
+
1231
+ for (let tp of Object.keys(this._codecs)) {
1232
+ const codec = this._codecs[tp];
1233
+ if (codec.encoder && aObject instanceof codec.type) {
1234
+ // TODO: what if multiple encoders found
1235
+ let encodedObj = await Promise.resolve(codec.encoder(aObject));
1236
+ if (encodedObj && !encodedObj._rtype) encodedObj._rtype = codec.name;
1237
+ // encode the functions in the interface object
1238
+ if (typeof encodedObj === "object") {
1239
+ const temp = encodedObj._rtype;
1240
+ delete encodedObj._rtype;
1241
+ encodedObj = await this._encode(
1242
+ encodedObj,
1243
+ session_id,
1244
+ local_workspace,
1245
+ );
1246
+ encodedObj._rtype = temp;
1247
+ }
1248
+ bObject = encodedObj;
1249
+ return bObject;
1250
+ }
1251
+ }
1252
+
1253
+ if (
1254
+ /*global tf*/
1255
+ typeof tf !== "undefined" &&
1256
+ tf.Tensor &&
1257
+ aObject instanceof tf.Tensor
1258
+ ) {
1259
+ const v_buffer = aObject.dataSync();
1260
+ bObject = {
1261
+ _rtype: "ndarray",
1262
+ _rvalue: new Uint8Array(v_buffer.buffer),
1263
+ _rshape: aObject.shape,
1264
+ _rdtype: aObject.dtype,
1265
+ };
1266
+ } else if (
1267
+ /*global nj*/
1268
+ typeof nj !== "undefined" &&
1269
+ nj.NdArray &&
1270
+ aObject instanceof nj.NdArray
1271
+ ) {
1272
+ const dtype = typedArrayToDtype(aObject.selection.data);
1273
+ bObject = {
1274
+ _rtype: "ndarray",
1275
+ _rvalue: new Uint8Array(aObject.selection.data.buffer),
1276
+ _rshape: aObject.shape,
1277
+ _rdtype: dtype,
1278
+ };
1279
+ } else if (aObject instanceof Error) {
1280
+ console.error(aObject);
1281
+ bObject = {
1282
+ _rtype: "error",
1283
+ _rvalue: aObject.toString(),
1284
+ _rtrace: aObject.stack,
1285
+ };
1286
+ }
1287
+ // send objects supported by structure clone algorithm
1288
+ // https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
1289
+ else if (
1290
+ aObject !== Object(aObject) ||
1291
+ aObject instanceof Boolean ||
1292
+ aObject instanceof String ||
1293
+ aObject instanceof Date ||
1294
+ aObject instanceof RegExp ||
1295
+ aObject instanceof ImageData ||
1296
+ (typeof FileList !== "undefined" && aObject instanceof FileList) ||
1297
+ (typeof FileSystemDirectoryHandle !== "undefined" &&
1298
+ aObject instanceof FileSystemDirectoryHandle) ||
1299
+ (typeof FileSystemFileHandle !== "undefined" &&
1300
+ aObject instanceof FileSystemFileHandle) ||
1301
+ (typeof FileSystemHandle !== "undefined" &&
1302
+ aObject instanceof FileSystemHandle) ||
1303
+ (typeof FileSystemWritableFileStream !== "undefined" &&
1304
+ aObject instanceof FileSystemWritableFileStream)
1305
+ ) {
1306
+ bObject = aObject;
1307
+ // TODO: avoid object such as DynamicPlugin instance.
1308
+ } else if (aObject instanceof Blob) {
1309
+ let _current_pos = 0;
1310
+ async function read(length) {
1311
+ let blob;
1312
+ if (length) {
1313
+ blob = aObject.slice(_current_pos, _current_pos + length);
1314
+ } else {
1315
+ blob = aObject.slice(_current_pos);
1316
+ }
1317
+ const ret = new Uint8Array(await blob.arrayBuffer());
1318
+ _current_pos = _current_pos + ret.byteLength;
1319
+ return ret;
1320
+ }
1321
+ function seek(pos) {
1322
+ _current_pos = pos;
1323
+ }
1324
+ bObject = {
1325
+ _rtype: "iostream",
1326
+ _rnative: "js:blob",
1327
+ type: aObject.type,
1328
+ name: aObject.name,
1329
+ size: aObject.size,
1330
+ path: aObject._path || aObject.webkitRelativePath,
1331
+ read: await this._encode(read, session_id, local_workspace),
1332
+ seek: await this._encode(seek, session_id, local_workspace),
1333
+ };
1334
+ } else if (aObject instanceof ArrayBufferView) {
1335
+ const dtype = typedArrayToDtype(aObject);
1336
+ bObject = {
1337
+ _rtype: "typedarray",
1338
+ _rvalue: new Uint8Array(aObject.buffer),
1339
+ _rdtype: dtype,
1340
+ };
1341
+ } else if (aObject instanceof DataView) {
1342
+ bObject = {
1343
+ _rtype: "memoryview",
1344
+ _rvalue: new Uint8Array(aObject.buffer),
1345
+ };
1346
+ } else if (aObject instanceof Set) {
1347
+ bObject = {
1348
+ _rtype: "set",
1349
+ _rvalue: await this._encode(
1350
+ Array.from(aObject),
1351
+ session_id,
1352
+ local_workspace,
1353
+ ),
1354
+ };
1355
+ } else if (aObject instanceof Map) {
1356
+ bObject = {
1357
+ _rtype: "orderedmap",
1358
+ _rvalue: await this._encode(
1359
+ Array.from(aObject),
1360
+ session_id,
1361
+ local_workspace,
1362
+ ),
1363
+ };
1364
+ } else if (
1365
+ aObject.constructor instanceof Object ||
1366
+ Array.isArray(aObject)
1367
+ ) {
1368
+ bObject = isarray ? [] : {};
1369
+ const keys = Object.keys(aObject);
1370
+ for (let k of keys) {
1371
+ bObject[k] = await this._encode(
1372
+ aObject[k],
1373
+ session_id,
1374
+ local_workspace,
1375
+ );
1376
+ }
1377
+ } else {
1378
+ throw `hypha-rpc: Unsupported data type: ${aObject}, you can register a custom codec to encode/decode the object.`;
1379
+ }
1380
+
1381
+ if (!bObject) {
1382
+ throw new Error("Failed to encode object");
1383
+ }
1384
+ return bObject;
1385
+ }
1386
+
1387
+ async decode(aObject) {
1388
+ return await this._decode(aObject);
1389
+ }
1390
+
1391
+ async _decode(
1392
+ aObject,
1393
+ remote_parent,
1394
+ local_parent,
1395
+ remote_workspace,
1396
+ local_workspace,
1397
+ ) {
1398
+ if (!aObject) {
1399
+ return aObject;
1400
+ }
1401
+ let bObject;
1402
+ if (aObject._rtype) {
1403
+ if (
1404
+ this._codecs[aObject._rtype] &&
1405
+ this._codecs[aObject._rtype].decoder
1406
+ ) {
1407
+ const temp = aObject._rtype;
1408
+ delete aObject._rtype;
1409
+ aObject = await this._decode(
1410
+ aObject,
1411
+ remote_parent,
1412
+ local_parent,
1413
+ remote_workspace,
1414
+ local_workspace,
1415
+ );
1416
+ aObject._rtype = temp;
1417
+
1418
+ bObject = await Promise.resolve(
1419
+ this._codecs[aObject._rtype].decoder(aObject),
1420
+ );
1421
+ } else if (aObject._rtype === "method") {
1422
+ bObject = this._generate_remote_method(
1423
+ aObject,
1424
+ remote_parent,
1425
+ local_parent,
1426
+ remote_workspace,
1427
+ local_workspace,
1428
+ );
1429
+ } else if (aObject._rtype === "ndarray") {
1430
+ /*global nj tf*/
1431
+ //create build array/tensor if used in the plugin
1432
+ if (typeof nj !== "undefined" && nj.array) {
1433
+ if (Array.isArray(aObject._rvalue)) {
1434
+ aObject._rvalue = aObject._rvalue.reduce(_appendBuffer);
1435
+ }
1436
+ bObject = nj
1437
+ .array(new Uint8(aObject._rvalue), aObject._rdtype)
1438
+ .reshape(aObject._rshape);
1439
+ } else if (typeof tf !== "undefined" && tf.Tensor) {
1440
+ if (Array.isArray(aObject._rvalue)) {
1441
+ aObject._rvalue = aObject._rvalue.reduce(_appendBuffer);
1442
+ }
1443
+ const arraytype = dtypeToTypedArray[aObject._rdtype];
1444
+ bObject = tf.tensor(
1445
+ new arraytype(aObject._rvalue),
1446
+ aObject._rshape,
1447
+ aObject._rdtype,
1448
+ );
1449
+ } else {
1450
+ //keep it as regular if transfered to the main app
1451
+ bObject = aObject;
1452
+ }
1453
+ } else if (aObject._rtype === "error") {
1454
+ bObject = new Error(
1455
+ "RemoteError: " + aObject._rvalue + "\n" + (aObject._rtrace || ""),
1456
+ );
1457
+ } else if (aObject._rtype === "typedarray") {
1458
+ const arraytype = dtypeToTypedArray[aObject._rdtype];
1459
+ if (!arraytype)
1460
+ throw new Error("unsupported dtype: " + aObject._rdtype);
1461
+ const buffer = aObject._rvalue.buffer.slice(
1462
+ aObject._rvalue.byteOffset,
1463
+ aObject._rvalue.byteOffset + aObject._rvalue.byteLength,
1464
+ );
1465
+ bObject = new arraytype(buffer);
1466
+ } else if (aObject._rtype === "memoryview") {
1467
+ bObject = aObject._rvalue.buffer.slice(
1468
+ aObject._rvalue.byteOffset,
1469
+ aObject._rvalue.byteOffset + aObject._rvalue.byteLength,
1470
+ ); // ArrayBuffer
1471
+ } else if (aObject._rtype === "iostream") {
1472
+ if (aObject._rnative === "js:blob") {
1473
+ const read = await this._generate_remote_method(
1474
+ aObject.read,
1475
+ remote_parent,
1476
+ local_parent,
1477
+ remote_workspace,
1478
+ local_workspace,
1479
+ );
1480
+ const bytes = await read();
1481
+ bObject = new Blob([bytes], {
1482
+ type: aObject.type,
1483
+ name: aObject.name,
1484
+ });
1485
+ } else {
1486
+ bObject = {};
1487
+ for (let k of Object.keys(aObject)) {
1488
+ if (!k.startsWith("_")) {
1489
+ bObject[k] = await this._decode(
1490
+ aObject[k],
1491
+ remote_parent,
1492
+ local_parent,
1493
+ remote_workspace,
1494
+ local_workspace,
1495
+ );
1496
+ }
1497
+ }
1498
+ }
1499
+ bObject["__rpc_object__"] = aObject;
1500
+ } else if (aObject._rtype === "orderedmap") {
1501
+ bObject = new Map(
1502
+ await this._decode(
1503
+ aObject._rvalue,
1504
+ remote_parent,
1505
+ local_parent,
1506
+ remote_workspace,
1507
+ local_workspace,
1508
+ ),
1509
+ );
1510
+ } else if (aObject._rtype === "set") {
1511
+ bObject = new Set(
1512
+ await this._decode(
1513
+ aObject._rvalue,
1514
+ remote_parent,
1515
+ local_parent,
1516
+ remote_workspace,
1517
+ local_workspace,
1518
+ ),
1519
+ );
1520
+ } else {
1521
+ const temp = aObject._rtype;
1522
+ delete aObject._rtype;
1523
+ bObject = await this._decode(
1524
+ aObject,
1525
+ remote_parent,
1526
+ local_parent,
1527
+ remote_workspace,
1528
+ local_workspace,
1529
+ );
1530
+ bObject._rtype = temp;
1531
+ }
1532
+ } else if (aObject.constructor === Object || Array.isArray(aObject)) {
1533
+ const isarray = Array.isArray(aObject);
1534
+ bObject = isarray ? [] : {};
1535
+ for (let k of Object.keys(aObject)) {
1536
+ if (isarray || aObject.hasOwnProperty(k)) {
1537
+ const v = aObject[k];
1538
+ bObject[k] = await this._decode(
1539
+ v,
1540
+ remote_parent,
1541
+ local_parent,
1542
+ remote_workspace,
1543
+ local_workspace,
1544
+ );
1545
+ }
1546
+ }
1547
+ } else {
1548
+ bObject = aObject;
1549
+ }
1550
+ if (bObject === undefined) {
1551
+ throw new Error("Failed to decode object");
1552
+ }
1553
+ return bObject;
1554
+ }
1555
+ }