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/.eslintrc +4 -0
- package/.prettierignore +3 -0
- package/LICENSE +21 -0
- package/README.md +27 -0
- package/dist/hypha-rpc-websocket.js +4688 -0
- package/dist/hypha-rpc-websocket.js.map +1 -0
- package/dist/hypha-rpc-websocket.min.js +2 -0
- package/dist/hypha-rpc-websocket.min.js.map +1 -0
- package/index.d.ts +96 -0
- package/index.js +1 -0
- package/karma.conf.js +114 -0
- package/package.json +65 -0
- package/report.html +39 -0
- package/src/rpc.js +1555 -0
- package/src/utils.js +331 -0
- package/src/webrtc-client.js +201 -0
- package/src/websocket-client.js +558 -0
- package/tests/.eslintrc.js +8 -0
- package/tests/websocket_client_test.js +203 -0
- package/webpack.config.js +44 -0
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
|
+
}
|