/
index.js
91 lines (88 loc) · 2.95 KB
/
index.js
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
/** TODO:
* - pooling (+ load balancing by tracking # of open calls)
* - queueing (worth it? sortof free via postMessage already)
*
* @example
* let worker = workerize(`
* export function add(a, b) {
* // block for a quarter of a second to demonstrate asynchronicity
* let start = Date.now();
* while (Date.now()-start < 250);
* return a + b;
* }
* `);
* (async () => {
* console.log('3 + 9 = ', await worker.add(3, 9));
* console.log('1 + 2 = ', await worker.add(1, 2));
* })();
*/
export default function workerize(code, options) {
let exports = {};
let exportsObjName = `__xpo${Math.random().toString().substring(2)}__`;
if (typeof code==='function') code = `(${Function.prototype.toString.call(code)})(${exportsObjName})`;
code = toCjs(code, exportsObjName, exports) + `\n(${Function.prototype.toString.call(setup)})(self,${exportsObjName},{})`;
let url = URL.createObjectURL(new Blob([code])),
worker = new Worker(url, options),
term = worker.terminate,
callbacks = {},
counter = 0,
i;
worker.kill = signal => {
worker.postMessage({ type: 'KILL', signal });
setTimeout(worker.terminate);
};
worker.terminate = () => {
URL.revokeObjectURL(url);
term.call(worker);
};
worker.call = (method, params) => new Promise( (resolve, reject) => {
let id = `rpc${++counter}`;
callbacks[id] = [resolve, reject];
worker.postMessage({ type: 'RPC', id, method, params });
});
worker.rpcMethods = {};
setup(worker, worker.rpcMethods, callbacks);
worker.expose = methodName => {
worker[methodName] = function() {
return worker.call(methodName, [].slice.call(arguments));
};
};
for (i in exports) if (!(i in worker)) worker.expose(i);
return worker;
}
function setup(ctx, rpcMethods, callbacks) {
ctx.addEventListener('message', ({ data }) => {
let id = data.id;
if (data.type!=='RPC' || id==null) return;
if (data.method) {
let method = rpcMethods[data.method];
if (method==null) {
ctx.postMessage({ type: 'RPC', id, error: 'NO_SUCH_METHOD' });
}
else {
Promise.resolve()
.then( () => method.apply(null, data.params) )
.then( result => { ctx.postMessage({ type: 'RPC', id, result }); })
.catch( err => { ctx.postMessage({ type: 'RPC', id, error: ''+err }); });
}
}
else {
let callback = callbacks[id];
if (callback==null) throw Error(`Unknown callback ${id}`);
delete callbacks[id];
if (data.error) callback[1](Error(data.error));
else callback[0](data.result);
}
});
}
function toCjs(code, exportsObjName, exports) {
code = code.replace(/^(\s*)export\s+default\s+/m, (s, before) => {
exports.default = true;
return `${before}${exportsObjName}.default=`;
});
code = code.replace(/^(\s*)export\s+((?:async\s*)?function(?:\s*\*)?|const|let|var)(\s+)([a-zA-Z$_][a-zA-Z0-9$_]*)/mg, (s, before, type, ws, name) => {
exports[name] = true;
return `${before}${exportsObjName}.${name}=${type}${ws}${name}`;
});
return `var ${exportsObjName}={};\n${code}\n${exportsObjName};`;
}