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/utils.js ADDED
@@ -0,0 +1,331 @@
1
+ export function randId() {
2
+ return Math.random().toString(36).substr(2, 10) + new Date().getTime();
3
+ }
4
+
5
+ export const dtypeToTypedArray = {
6
+ int8: Int8Array,
7
+ int16: Int16Array,
8
+ int32: Int32Array,
9
+ uint8: Uint8Array,
10
+ uint16: Uint16Array,
11
+ uint32: Uint32Array,
12
+ float32: Float32Array,
13
+ float64: Float64Array,
14
+ array: Array,
15
+ };
16
+
17
+ export async function loadRequirementsInWindow(requirements) {
18
+ function _importScript(url) {
19
+ //url is URL of external file, implementationCode is the code
20
+ //to be called from the file, location is the location to
21
+ //insert the <script> element
22
+ return new Promise((resolve, reject) => {
23
+ var scriptTag = document.createElement("script");
24
+ scriptTag.src = url;
25
+ scriptTag.type = "text/javascript";
26
+ scriptTag.onload = resolve;
27
+ scriptTag.onreadystatechange = function () {
28
+ if (this.readyState === "loaded" || this.readyState === "complete") {
29
+ resolve();
30
+ }
31
+ };
32
+ scriptTag.onerror = reject;
33
+ document.head.appendChild(scriptTag);
34
+ });
35
+ }
36
+
37
+ // support importScripts outside web worker
38
+ async function importScripts() {
39
+ var args = Array.prototype.slice.call(arguments),
40
+ len = args.length,
41
+ i = 0;
42
+ for (; i < len; i++) {
43
+ await _importScript(args[i]);
44
+ }
45
+ }
46
+
47
+ if (
48
+ requirements &&
49
+ (Array.isArray(requirements) || typeof requirements === "string")
50
+ ) {
51
+ try {
52
+ var link_node;
53
+ requirements =
54
+ typeof requirements === "string" ? [requirements] : requirements;
55
+ if (Array.isArray(requirements)) {
56
+ for (var i = 0; i < requirements.length; i++) {
57
+ if (
58
+ requirements[i].toLowerCase().endsWith(".css") ||
59
+ requirements[i].startsWith("css:")
60
+ ) {
61
+ if (requirements[i].startsWith("css:")) {
62
+ requirements[i] = requirements[i].slice(4);
63
+ }
64
+ link_node = document.createElement("link");
65
+ link_node.rel = "stylesheet";
66
+ link_node.href = requirements[i];
67
+ document.head.appendChild(link_node);
68
+ } else if (
69
+ requirements[i].toLowerCase().endsWith(".mjs") ||
70
+ requirements[i].startsWith("mjs:")
71
+ ) {
72
+ // import esmodule
73
+ if (requirements[i].startsWith("mjs:")) {
74
+ requirements[i] = requirements[i].slice(4);
75
+ }
76
+ await import(/* webpackIgnore: true */ requirements[i]);
77
+ } else if (
78
+ requirements[i].toLowerCase().endsWith(".js") ||
79
+ requirements[i].startsWith("js:")
80
+ ) {
81
+ if (requirements[i].startsWith("js:")) {
82
+ requirements[i] = requirements[i].slice(3);
83
+ }
84
+ await importScripts(requirements[i]);
85
+ } else if (requirements[i].startsWith("http")) {
86
+ await importScripts(requirements[i]);
87
+ } else if (requirements[i].startsWith("cache:")) {
88
+ //ignore cache
89
+ } else {
90
+ console.log("Unprocessed requirements url: " + requirements[i]);
91
+ }
92
+ }
93
+ } else {
94
+ throw "unsupported requirements definition";
95
+ }
96
+ } catch (e) {
97
+ throw "failed to import required scripts: " + requirements.toString();
98
+ }
99
+ }
100
+ }
101
+
102
+ export async function loadRequirementsInWebworker(requirements) {
103
+ if (
104
+ requirements &&
105
+ (Array.isArray(requirements) || typeof requirements === "string")
106
+ ) {
107
+ try {
108
+ if (!Array.isArray(requirements)) {
109
+ requirements = [requirements];
110
+ }
111
+ for (var i = 0; i < requirements.length; i++) {
112
+ if (
113
+ requirements[i].toLowerCase().endsWith(".css") ||
114
+ requirements[i].startsWith("css:")
115
+ ) {
116
+ throw "unable to import css in a webworker";
117
+ } else if (
118
+ requirements[i].toLowerCase().endsWith(".js") ||
119
+ requirements[i].startsWith("js:")
120
+ ) {
121
+ if (requirements[i].startsWith("js:")) {
122
+ requirements[i] = requirements[i].slice(3);
123
+ }
124
+ importScripts(requirements[i]);
125
+ } else if (requirements[i].startsWith("http")) {
126
+ importScripts(requirements[i]);
127
+ } else if (requirements[i].startsWith("cache:")) {
128
+ //ignore cache
129
+ } else {
130
+ console.log("Unprocessed requirements url: " + requirements[i]);
131
+ }
132
+ }
133
+ } catch (e) {
134
+ throw "failed to import required scripts: " + requirements.toString();
135
+ }
136
+ }
137
+ }
138
+
139
+ export function loadRequirements(requirements) {
140
+ if (
141
+ typeof WorkerGlobalScope !== "undefined" &&
142
+ self instanceof WorkerGlobalScope
143
+ ) {
144
+ return loadRequirementsInWebworker(requirements);
145
+ } else {
146
+ return loadRequirementsInWindow(requirements);
147
+ }
148
+ }
149
+
150
+ export function normalizeConfig(config) {
151
+ config.version = config.version || "0.1.0";
152
+ config.description =
153
+ config.description || `[TODO: add description for ${config.name} ]`;
154
+ config.type = config.type || "rpc-window";
155
+ config.id = config.id || randId();
156
+ config.target_origin = config.target_origin || "*";
157
+ config.allow_execution = config.allow_execution || false;
158
+ // remove functions
159
+ config = Object.keys(config).reduce((p, c) => {
160
+ if (typeof config[c] !== "function") p[c] = config[c];
161
+ return p;
162
+ }, {});
163
+ return config;
164
+ }
165
+ export const typedArrayToDtypeMapping = {
166
+ Int8Array: "int8",
167
+ Int16Array: "int16",
168
+ Int32Array: "int32",
169
+ Uint8Array: "uint8",
170
+ Uint16Array: "uint16",
171
+ Uint32Array: "uint32",
172
+ Float32Array: "float32",
173
+ Float64Array: "float64",
174
+ Array: "array",
175
+ };
176
+
177
+ const typedArrayToDtypeKeys = [];
178
+ for (const arrType of Object.keys(typedArrayToDtypeMapping)) {
179
+ typedArrayToDtypeKeys.push(eval(arrType));
180
+ }
181
+
182
+ export function typedArrayToDtype(obj) {
183
+ let dtype = typedArrayToDtypeMapping[obj.constructor.name];
184
+ if (!dtype) {
185
+ const pt = Object.getPrototypeOf(obj);
186
+ for (const arrType of typedArrayToDtypeKeys) {
187
+ if (pt instanceof arrType) {
188
+ dtype = typedArrayToDtypeMapping[arrType.name];
189
+ break;
190
+ }
191
+ }
192
+ }
193
+ return dtype;
194
+ }
195
+
196
+ function cacheUrlInServiceWorker(url) {
197
+ return new Promise(function (resolve, reject) {
198
+ const message = {
199
+ command: "add",
200
+ url: url,
201
+ };
202
+ if (!navigator.serviceWorker || !navigator.serviceWorker.register) {
203
+ reject("Service worker is not supported.");
204
+ return;
205
+ }
206
+ const messageChannel = new MessageChannel();
207
+ messageChannel.port1.onmessage = function (event) {
208
+ if (event.data && event.data.error) {
209
+ reject(event.data.error);
210
+ } else {
211
+ resolve(event.data && event.data.result);
212
+ }
213
+ };
214
+
215
+ if (navigator.serviceWorker && navigator.serviceWorker.controller) {
216
+ navigator.serviceWorker.controller.postMessage(message, [
217
+ messageChannel.port2,
218
+ ]);
219
+ } else {
220
+ reject("Service worker controller is not available");
221
+ }
222
+ });
223
+ }
224
+
225
+ export async function cacheRequirements(requirements) {
226
+ requirements = requirements || [];
227
+ if (!Array.isArray(requirements)) {
228
+ requirements = [requirements];
229
+ }
230
+ for (let req of requirements) {
231
+ //remove prefix
232
+ if (req.startsWith("js:")) req = req.slice(3);
233
+ if (req.startsWith("css:")) req = req.slice(4);
234
+ if (req.startsWith("cache:")) req = req.slice(6);
235
+ if (!req.startsWith("http")) continue;
236
+
237
+ await cacheUrlInServiceWorker(req).catch((e) => {
238
+ console.error(e);
239
+ });
240
+ }
241
+ }
242
+
243
+ export function assert(condition, message) {
244
+ if (!condition) {
245
+ throw new Error(message || "Assertion failed");
246
+ }
247
+ }
248
+
249
+ //#Source https://bit.ly/2neWfJ2
250
+ export function urlJoin(...args) {
251
+ return args
252
+ .join("/")
253
+ .replace(/[\/]+/g, "/")
254
+ .replace(/^(.+):\//, "$1://")
255
+ .replace(/^file:/, "file:/")
256
+ .replace(/\/(\?|&|#[^!])/g, "$1")
257
+ .replace(/\?/g, "&")
258
+ .replace("&", "?");
259
+ }
260
+
261
+ export function waitFor(prom, time, error) {
262
+ let timer;
263
+ return Promise.race([
264
+ prom,
265
+ new Promise(
266
+ (_r, rej) =>
267
+ (timer = setTimeout(() => {
268
+ rej(error || "Timeout Error");
269
+ }, time * 1000)),
270
+ ),
271
+ ]).finally(() => clearTimeout(timer));
272
+ }
273
+
274
+ export class MessageEmitter {
275
+ constructor(debug) {
276
+ this._event_handlers = {};
277
+ this._once_handlers = {};
278
+ this._debug = debug;
279
+ }
280
+ emit() {
281
+ throw new Error("emit is not implemented");
282
+ }
283
+ on(event, handler) {
284
+ if (!this._event_handlers[event]) {
285
+ this._event_handlers[event] = [];
286
+ }
287
+ this._event_handlers[event].push(handler);
288
+ }
289
+ once(event, handler) {
290
+ handler.___event_run_once = true;
291
+ this.on(event, handler);
292
+ }
293
+ off(event, handler) {
294
+ if (!event && !handler) {
295
+ // remove all events handlers
296
+ this._event_handlers = {};
297
+ } else if (event && !handler) {
298
+ // remove all hanlders for the event
299
+ if (this._event_handlers[event]) this._event_handlers[event] = [];
300
+ } else {
301
+ // remove a specific handler
302
+ if (this._event_handlers[event]) {
303
+ const idx = this._event_handlers[event].indexOf(handler);
304
+ if (idx >= 0) {
305
+ this._event_handlers[event].splice(idx, 1);
306
+ }
307
+ }
308
+ }
309
+ }
310
+ _fire(event, data) {
311
+ if (this._event_handlers[event]) {
312
+ var i = this._event_handlers[event].length;
313
+ while (i--) {
314
+ const handler = this._event_handlers[event][i];
315
+ try {
316
+ handler(data);
317
+ } catch (e) {
318
+ console.error(e);
319
+ } finally {
320
+ if (handler.___event_run_once) {
321
+ this._event_handlers[event].splice(i, 1);
322
+ }
323
+ }
324
+ }
325
+ } else {
326
+ if (this._debug) {
327
+ console.warn("unhandled event", event, data);
328
+ }
329
+ }
330
+ }
331
+ }
@@ -0,0 +1,201 @@
1
+ import { RPC } from "./rpc.js";
2
+ import { assert, randId } from "./utils.js";
3
+
4
+ class WebRTCConnection {
5
+ constructor(channel) {
6
+ this._data_channel = channel;
7
+ this._handle_message = null;
8
+ this._reconnection_token = null;
9
+ this._data_channel.onmessage = async (event) => {
10
+ let data = event.data;
11
+ if (data instanceof Blob) {
12
+ data = await data.arrayBuffer();
13
+ }
14
+ this._handle_message(data);
15
+ };
16
+ const self = this;
17
+ this._data_channel.onclose = function () {
18
+ console.log("websocket closed");
19
+ self._data_channel = null;
20
+ };
21
+ }
22
+
23
+ on_message(handler) {
24
+ assert(handler, "handler is required");
25
+ this._handle_message = handler;
26
+ }
27
+
28
+ async emit_message(data) {
29
+ assert(this._handle_message, "No handler for message");
30
+ try {
31
+ this._data_channel.send(data);
32
+ } catch (exp) {
33
+ // data = msgpack_unpackb(data);
34
+ console.error(`Failed to send data, error: ${exp}`);
35
+ throw exp;
36
+ }
37
+ }
38
+
39
+ async disconnect(reason) {
40
+ this._data_channel = null;
41
+ console.info(`data channel connection disconnected (${reason})`);
42
+ }
43
+ }
44
+
45
+ async function _setupRPC(config) {
46
+ assert(config.channel, "No channel provided");
47
+ assert(config.workspace, "No workspace provided");
48
+ const channel = config.channel;
49
+ const clientId = config.client_id || randId();
50
+ const connection = new WebRTCConnection(channel);
51
+ config.context = config.context || {};
52
+ config.context.connection_type = "webrtc";
53
+ const rpc = new RPC(connection, {
54
+ client_id: clientId,
55
+ manager_id: null,
56
+ default_context: config.context,
57
+ name: config.name,
58
+ method_timeout: config.method_timeout || 10.0,
59
+ workspace: config.workspace,
60
+ });
61
+ return rpc;
62
+ }
63
+
64
+ async function _createOffer(params, server, config, onInit, context) {
65
+ config = config || {};
66
+ let offer = new RTCSessionDescription({
67
+ sdp: params.sdp,
68
+ type: params.type,
69
+ });
70
+
71
+ let pc = new RTCPeerConnection({
72
+ iceServers: config.ice_servers || [
73
+ { urls: ["stun:stun.l.google.com:19302"] },
74
+ ],
75
+ sdpSemantics: "unified-plan",
76
+ });
77
+
78
+ if (server) {
79
+ pc.addEventListener("datachannel", async (event) => {
80
+ const channel = event.channel;
81
+ let ctx = null;
82
+ if (context && context.user) ctx = { user: context.user };
83
+ const rpc = await _setupRPC({
84
+ channel: channel,
85
+ client_id: channel.label,
86
+ workspace: server.config.workspace,
87
+ context: ctx,
88
+ });
89
+ // Map all the local services to the webrtc client
90
+ rpc._services = server.rpc._services;
91
+ });
92
+ }
93
+
94
+ if (onInit) {
95
+ await onInit(pc);
96
+ }
97
+
98
+ await pc.setRemoteDescription(offer);
99
+
100
+ let answer = await pc.createAnswer();
101
+ await pc.setLocalDescription(answer);
102
+
103
+ return {
104
+ sdp: pc.localDescription.sdp,
105
+ type: pc.localDescription.type,
106
+ workspace: server.config.workspace,
107
+ };
108
+ }
109
+
110
+ async function getRTCService(server, service_id, config) {
111
+ config = config || {};
112
+ config.peer_id = config.peer_id || randId();
113
+
114
+ const pc = new RTCPeerConnection({
115
+ iceServers: config.ice_servers || [
116
+ { urls: ["stun:stun.l.google.com:19302"] },
117
+ ],
118
+ sdpSemantics: "unified-plan",
119
+ });
120
+
121
+ return new Promise(async (resolve, reject) => {
122
+ try {
123
+ pc.addEventListener(
124
+ "connectionstatechange",
125
+ () => {
126
+ if (pc.connectionState === "failed") {
127
+ pc.close();
128
+ reject(new Error("Connection failed"));
129
+ }
130
+ },
131
+ false,
132
+ );
133
+
134
+ if (config.on_init) {
135
+ await config.on_init(pc);
136
+ delete config.on_init;
137
+ }
138
+ let channel = pc.createDataChannel(config.peer_id, { ordered: true });
139
+ channel.binaryType = "arraybuffer";
140
+ const offer = await pc.createOffer();
141
+ await pc.setLocalDescription(offer);
142
+ const svc = await server.getService(service_id);
143
+ const answer = await svc.offer({
144
+ sdp: pc.localDescription.sdp,
145
+ type: pc.localDescription.type,
146
+ });
147
+
148
+ channel.onopen = () => {
149
+ config.channel = channel;
150
+ config.workspace = answer.workspace;
151
+ // Wait for the channel to be open before returning the rpc
152
+ // This is needed for safari to work
153
+ setTimeout(async () => {
154
+ const rpc = await _setupRPC(config);
155
+ pc.rpc = rpc;
156
+ async function getService(name) {
157
+ return await rpc.get_remote_service(config.peer_id + ":" + name);
158
+ }
159
+ async function disconnect() {
160
+ await rpc.disconnect();
161
+ pc.close();
162
+ }
163
+ pc.get_service = getService;
164
+ pc.getService = getService;
165
+ pc.disconnect = disconnect;
166
+ pc.register_codec = rpc.register_codec;
167
+ pc.registerCodec = rpc.register_codec;
168
+ resolve(pc);
169
+ }, 500);
170
+ };
171
+
172
+ channel.onclose = () => reject(new Error("Data channel closed"));
173
+
174
+ await pc.setRemoteDescription(
175
+ new RTCSessionDescription({
176
+ sdp: answer.sdp,
177
+ type: answer.type,
178
+ }),
179
+ );
180
+ } catch (e) {
181
+ reject(e);
182
+ }
183
+ });
184
+ }
185
+
186
+ async function registerRTCService(server, service_id, config) {
187
+ config = config || {
188
+ visibility: "protected",
189
+ require_context: true,
190
+ };
191
+ const onInit = config.on_init;
192
+ delete config.on_init;
193
+ await server.registerService({
194
+ id: service_id,
195
+ config,
196
+ offer: (params, context) =>
197
+ _createOffer(params, server, config, onInit, context),
198
+ });
199
+ }
200
+
201
+ export { getRTCService, registerRTCService };