Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to pass a function #37

Open
bestwestern opened this issue Mar 29, 2019 · 6 comments
Open

How to pass a function #37

bestwestern opened this issue Mar 29, 2019 · 6 comments

Comments

@bestwestern
Copy link

Usage
Pass either a function or a string containing code.

Could you please show in the example how to pass a function?

@ChristianMurphy
Copy link

https://github.com/GoogleChromeLabs/comlink#comlinkproxyvaluevalue could be one option

@chase-moskal
Copy link

chase-moskal commented Apr 29, 2019

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?

@bestwestern
Copy link
Author

@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 ;)

@kairos666
Copy link

kairos666 commented Oct 5, 2019

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));
})();

@dandv
Copy link

dandv commented Oct 19, 2019

@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?

@developit
Copy link
Owner

developit commented Dec 28, 2019

This turned into documentation, my apologies.

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.

Broken: "Function exports" (not a thing)

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 :(
})

Solution: use CommonJS

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

Automate exports using a comment annotation

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.

Automate exports by parsing with a Regex

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)

Alternative: Use Comlink

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'

Alternative 2: Workerize implemented using Comlink

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:

https://jsfiddle.net/developit/kr5j1d6v/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants