-
Notifications
You must be signed in to change notification settings - Fork 71
/
LibraryUtils.ts
270 lines (237 loc) · 9.99 KB
/
LibraryUtils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
import assert from "assert";
import GenUtils from "./GenUtils";
import MoneroError from "./MoneroError";
import ThreadPool from "./ThreadPool";
import path from "path";
/**
* Collection of helper utilities for the library.
*/
export default class LibraryUtils {
// static variables
static LOG_LEVEL = 0;
static WASM_MODULE: any;
static WORKER: any;
static WORKER_OBJECTS: any;
static FULL_LOADED: any;
static REJECT_UNAUTHORIZED_FNS: any;
static WORKER_DIST_PATH_DEFAULT = GenUtils.isBrowser() ? "/monero_web_worker.js" : function() {
// get worker path in dist (assumes library is running from src or dist)
let curPath = path.normalize(__dirname);
const targetPath = path.join('monero-ts', 'dist');
if (!curPath.includes(targetPath)) curPath = path.join(curPath, "../../../../dist/src/main/js/common");
return LibraryUtils.prefixWindowsPath(path.join(curPath, "./MoneroWebWorker.js"));
}();
static WORKER_DIST_PATH = LibraryUtils.WORKER_DIST_PATH_DEFAULT;
/**
* Log a message.
*
* @param {number} level - log level of the message
* @param {string} msg - message to log
*/
static log(level, msg) {
assert(level === parseInt(level, 10) && level >= 0, "Log level must be an integer >= 0");
if (LibraryUtils.LOG_LEVEL >= level) console.log(msg);
}
/**
* Set the library's log level with 0 being least verbose.
*
* @param {number} level - the library's log level
*/
static async setLogLevel(level) {
assert(level === parseInt(level, 10) && level >= 0, "Log level must be an integer >= 0");
LibraryUtils.LOG_LEVEL = level;
if (LibraryUtils.WASM_MODULE) LibraryUtils.WASM_MODULE.set_log_level(level);
if (LibraryUtils.WORKER) await LibraryUtils.invokeWorker(undefined, "setLogLevel", [level]);
}
/**
* Get the library's log level.
*
* @return {number} the library's log level
*/
static getLogLevel(): number {
return LibraryUtils.LOG_LEVEL;
}
/**
* Get the total memory used by WebAssembly.
*
* @return {Promise<number>} the total memory used by WebAssembly
*/
static async getWasmMemoryUsed(): Promise<number> {
let total = 0;
if (LibraryUtils.WORKER) total += await LibraryUtils.invokeWorker(undefined, "getWasmMemoryUsed", []) as number;
if (LibraryUtils.getWasmModule() && LibraryUtils.getWasmModule().HEAP8) total += LibraryUtils.getWasmModule().HEAP8.length;
return total;
}
/**
* Get the WebAssembly module in the current context (nodejs, browser main thread or worker).
*/
static getWasmModule() {
return LibraryUtils.WASM_MODULE;
}
/**
* Load the WebAssembly keys module with caching.
*/
static async loadKeysModule() {
// use cache if suitable, full module supersedes keys module because it is superset
if (LibraryUtils.WASM_MODULE) return LibraryUtils.WASM_MODULE;
// load module
let module = await require("../../../../dist/monero_wallet_keys")();
LibraryUtils.WASM_MODULE = module
delete LibraryUtils.WASM_MODULE.then;
LibraryUtils.initWasmModule(LibraryUtils.WASM_MODULE);
return module;
}
/**
* Load the WebAssembly full module with caching.
*
* The full module is a superset of the keys module and overrides it.
*
* TODO: this is separate static function from loadKeysModule() because webpack cannot bundle worker using runtime param for conditional import
*/
static async loadFullModule() {
// use cache if suitable, full module supersedes keys module because it is superset
if (LibraryUtils.WASM_MODULE && LibraryUtils.FULL_LOADED) return LibraryUtils.WASM_MODULE;
// load module
let module = await require("../../../../dist/monero_wallet_full")();
LibraryUtils.WASM_MODULE = module
delete LibraryUtils.WASM_MODULE.then;
LibraryUtils.FULL_LOADED = true;
LibraryUtils.initWasmModule(LibraryUtils.WASM_MODULE);
return module;
}
/**
* Register a function by id which informs if unauthorized requests (e.g.
* self-signed certificates) should be rejected.
*
* @param {string} fnId - unique identifier for the function
* @param {function} fn - function to inform if unauthorized requests should be rejected
*/
static setRejectUnauthorizedFn(fnId, fn) {
if (!LibraryUtils.REJECT_UNAUTHORIZED_FNS) LibraryUtils.REJECT_UNAUTHORIZED_FNS = [];
if (fn === undefined) delete LibraryUtils.REJECT_UNAUTHORIZED_FNS[fnId];
else LibraryUtils.REJECT_UNAUTHORIZED_FNS[fnId] = fn;
}
/**
* Indicate if unauthorized requests should be rejected.
*
* @param {string} fnId - uniquely identifies the function
*/
static isRejectUnauthorized(fnId) {
if (!LibraryUtils.REJECT_UNAUTHORIZED_FNS[fnId]) throw new Error("No function registered with id " + fnId + " to inform if unauthorized reqs should be rejected");
return LibraryUtils.REJECT_UNAUTHORIZED_FNS[fnId]();
}
/**
* Set the path to load the worker. Defaults to "/monero_web_worker.js" in the browser
* and "./MoneroWebWorker.js" in node.
*
* @param {string} workerDistPath - path to load the worker
*/
static setWorkerDistPath(workerDistPath) {
let path = LibraryUtils.prefixWindowsPath(workerDistPath ? workerDistPath : LibraryUtils.WORKER_DIST_PATH_DEFAULT);
if (path !== LibraryUtils.WORKER_DIST_PATH) delete LibraryUtils.WORKER;
LibraryUtils.WORKER_DIST_PATH = path;
}
/**
* Get a singleton instance of a worker to share.
*
* @return {Worker} a worker to share among wallet instances
*/
static async getWorker() {
// one time initialization
if (!LibraryUtils.WORKER) {
if (GenUtils.isBrowser()) {
LibraryUtils.WORKER = new Worker(LibraryUtils.WORKER_DIST_PATH);
} else {
const Worker = require("web-worker"); // import web worker if nodejs
LibraryUtils.WORKER = new Worker(LibraryUtils.WORKER_DIST_PATH);
}
LibraryUtils.WORKER_OBJECTS = {}; // store per object running in the worker
// receive worker errors
LibraryUtils.WORKER.onerror = function(err) {
console.error("Error posting message to Monero web worker; is it built and copied to the app's build directory (e.g. in the root)?");
console.log(err);
};
// receive worker messages
LibraryUtils.WORKER.onmessage = function(e) {
// lookup object id, callback function, and this arg
let thisArg = undefined;
let callbackFn = LibraryUtils.WORKER_OBJECTS[e.data[0]].callbacks[e.data[1]]; // look up by object id then by function name
if (callbackFn === undefined) throw new Error("No worker callback function defined for key '" + e.data[1] + "'");
if (callbackFn instanceof Array) { // this arg may be stored with callback function
thisArg = callbackFn[1];
callbackFn = callbackFn[0];
}
// invoke callback function with this arg and arguments
callbackFn.apply(thisArg, e.data.slice(2));
}
}
return LibraryUtils.WORKER;
}
static addWorkerCallback(objectId, callbackId, callbackArgs) {
LibraryUtils.WORKER_OBJECTS[objectId].callbacks[callbackId] = callbackArgs;
}
static removeWorkerCallback(objectId, callbackId) {
delete LibraryUtils.WORKER_OBJECTS[objectId].callbacks[callbackId];
}
static removeWorkerObject(objectId) {
delete LibraryUtils.WORKER_OBJECTS[objectId];
}
/**
* Terminate monero-ts's singleton worker.
*/
static async terminateWorker() {
if (LibraryUtils.WORKER) {
LibraryUtils.WORKER.terminate();
delete LibraryUtils.WORKER;
LibraryUtils.WORKER = undefined;
}
}
/**
* Invoke a worker function and get the result with error handling.
*
* @param {string} objectId identifies the worker object to invoke (default random id)
* @param {string} fnName is the name of the function to invoke
* @param {any[]} [args] are function arguments to invoke with
* @return {any} resolves with response payload from the worker or an error
*/
static async invokeWorker(objectId, fnName, args) {
assert(fnName.length >= 2);
let worker = await LibraryUtils.getWorker();
let randomObject = objectId === undefined;
if (randomObject) objectId = GenUtils.getUUID();
if (!LibraryUtils.WORKER_OBJECTS[objectId]) LibraryUtils.WORKER_OBJECTS[objectId] = {callbacks: {}};
let callbackId = GenUtils.getUUID();
try {
return await new Promise((resolve, reject) => {
LibraryUtils.WORKER_OBJECTS[objectId].callbacks[callbackId] = (resp) => { // TODO: this defines function once per callback
delete LibraryUtils.WORKER_OBJECTS[objectId].callbacks[callbackId];
if (randomObject) delete LibraryUtils.WORKER_OBJECTS[objectId];
resp ? (resp.error ? reject(new Error(JSON.stringify(resp.error))) : resolve(resp.result)) : resolve(undefined);
};
worker.postMessage([objectId, fnName, callbackId].concat(args === undefined ? [] : GenUtils.listify(args)));
});
} catch (e: any) {
throw LibraryUtils.deserializeError(JSON.parse(e.message));
}
}
static serializeError(err) {
const serializedErr: any = { name: err.name, message: err.message, stack: err.stack };
if (err instanceof MoneroError) serializedErr.type = "MoneroError";
return serializedErr;
}
protected static deserializeError(serializedErr) {
const err = serializedErr.type === "MoneroError" ? new MoneroError(serializedErr.message) : new Error(serializedErr.message);
err.name = serializedErr.name;
err.stack = err.stack + "\nWorker error: " + serializedErr.stack;
return err;
}
// ------------------------------ PRIVATE HELPERS ---------------------------
protected static initWasmModule(wasmModule) {
wasmModule.taskQueue = new ThreadPool(1);
wasmModule.queueTask = async function(asyncFn) { return wasmModule.taskQueue.submit(asyncFn); }
}
protected static prefixWindowsPath(path) {
if (/^[A-Z]:/.test(path) && path.indexOf("file://") == -1) path = "file://" + path; // prepend e.g. C: paths with file://
return path;
}
}