Open bestwestern opened 5 years ago
https://github.com/GoogleChromeLabs/comlink#comlinkproxyvaluevalue could be one option
huh? i don't get it either
@ChristianMurphy, the example you linked to shows: const api = Comlink.proxy(new Worker("worker.js"));
however "worker.js"
is a string — not a function
..does comlink have anything to do with workerize?
@ChristianMurphy thank you. @chase-moskal No - I think Christian proposed I used another library.
@developit is brilliant but he should get an intern to do documentation for mere mortals ;)
I've had the same problem and couldn't figure out how to directly use a function as parameter. Found a workaround though that may help you keep your code DRY.
function foo() { return 3; }
function bar(multiply) { return foo() * multiply; }
const inlineWorkerFile = `
${ foo.toString() }
export ${ bar.toString() }
`
let worker = workerize(inlineWorkerFile);
(async () => {
console.log('3 * 3 = ', await worker.bar(3));
console.log('3 * 1 = ', await worker.bar(1));
})();
@kairos666: the code seems to be doing pretty much what you're suggesting but when trying to actually pass a function, I see that the arguments are not passed to the created worker.
@surma, since you write a compelling advocacy post on using Workers, might you have a look at this?
Passing a function instead of a string is a little more involved than running that function in a Worker. Workerize functions by parsing the code for the string (or function) you pass to find ES Modules exports (like export function foo(){}
). It actually turns that code into something that looks like CommonJS before running it in the Worker, which is important for reasons I'll get to in a minute.
Unfortunately, there's no way to use exports within a Function - the export syntax only works at the root of a module and can't be inside of blocks or functions. So, while Workerize supports passing a function, it's not possible to export
things from a function, and without being exported Workerize can't find your nested functions and make them callable from the main thread:
const { a, b } = workerize(function() {
export function a() { return 1 }
export function b() { return 2 }
// ^^ these are Syntax Errors :(
})
So what can we do? As I mentioned above, Workerize transforms ES Modules exports into roughly CommonJS before running code in the Worker context. When passing a function, the first argument to that function is an exports
object, which you can decorate with named functions you want to make callable from the main thread!
There's one hiccup though - because we're skipping the ES Modules parsing step, Workerize needs to be told what function names we have available rather than inferring them from our code. For this, there's a method on all Workerize instances called expose()
, which takes a function name and defines it as method on the worker.
const worker = workerize(function(exports) {
exports.a = () => 1;
exports.b = () => 2;
});
worker.expose('a');
worker.expose('b');
worker.a().then(console.log) // 1
Obviously this is a little bit more manual than we'd like. However, there's a solution! Workerize's parsing of exports is extremely naive and can be easily tricked. It doesn't even know about comments. That means we can annotate our methods using a comment and have Workerize detect those exports!
const worker = workerize(function(exports) {
/*
export function a() {}
export function b() {}
*/
exports.a = () => 1;
exports.b = () => 2;
});
worker.a().then(console.log) // 1
I've created a demo of this code on JSFiddle.
One risk with the comment decoration approach is that most production minifier setups remove comments. That's definitely not good for us, since doing so would remove the "mirrored" versions of our exported functions on the main thread. Here's a (very) naive wrapper around Workerize that finds commonjs-style exports and exposes them on the Workerize instance:
function workerizeCjs(func) {
const w = workerize(func);
String(func).match(/\bexports\.[\w$]+/g).map(f=>w.expose(f.substr(8)));
return w;
}
(demo)
None of these are amazing solutions TBH. For runtime usage, Comlink has far more to offer here and isn't that much larger (it's around 1.1kb). Here's a version of Workerize that is built with Comlink:
// Assuming Comlink is in scope on the main thread...
function comlinker(func) {
return Comlink.wrap(new Worker(URL.createObjectURL(new Blob([
'importScripts("https://unpkg.com/comlink@4.2.0/dist/umd/comlink.min.js");var exports={},module={exports:exports};('+func+')(exports);Comlink.expose(module.exports);'
]))));
}
// same as workerize, but with all of Comlink's nice nesting+callback+transferables support:
const worker = comlinker(exports => {
exports.foo = async () => 'bar';
});
worker.foo().then(console.log); // 'bar'
It's a little awkward to have to load the Comlink dependency separately on the worker and main thread, so I created a demo showing a pared-down inline version of Comlink that shares itself with the Worker thread. That sharing bit is Workerize's main selling point, so you could consider this to be a version of Workerize implemented using Comlink:
Could you please show in the example how to pass a function?