xeolabs
xeolabs

Reputation: 1389

Can I spawn a Web Worker and inject JavaScript (created at runtime by parent) into it from the parent "process"?

Is it possible to spawn a web worker, and somehow inject parent-generated JavaScript into it from the parent thread? ie. without having to make the worker include a file, rather I want the parent to inject it somehow.

An example could be where the parent creates a bunch of math functions at run time, then creates a Web worker to run them.

Upvotes: 1

Views: 1766

Answers (2)

user128511
user128511

Reputation:

This question is almost 10yrs old so things have changed since it was asked. It was also asked without context on what it means to "inject functions". Do you want to inject single functions into an existing environment or do you want to run whole scripts or do you want to inject functions into scripts.

In any case, you can run a script via in a worker via a blob

const script = `
function doIt() {
  console.log('hello world');
}
doIt();
`;
const blob = new Blob([script], {type: 'application/javascript'});
const url = URL.createObjectURL(blob);
const worker = new Worker(url);

If you run the script above and check the JavaScript console you'll see it ran

Another solution is to run a service worker. A service worker effectively intercepts network requests from the page and lets you apply JavaScript to them before they are sent to the network itself.

So, effectively you can return your own strings from the service-worker in response to requests from the webpage or workers. You can put functions in those strings and you can provide the strings by sending them to the service-worker

In pseudo-code

// service-worker.js
let injectString = '';

self.addEventListener('fetch', (event) => {

  event.respondWith(async function() {
    const url = new URL(event.request.url);
    if (url.pathname === 'inject-fn.js')
      const response = new Response(injectString, {
        headers: {'Content-Type': 'application/javascript'},
      });
      return response;
    }
    return await fetch(event.request);
  }());
}

self.addEventListener('message', (event) => {
  injectString = event.data;
});

This makes it so if the worker requests 'inject-fn.js' then it will receive the string stored in injectString, otherwise it will go the network as normal

To set the string you post a message to the service-worker and the load the script

  const worker = navigator.serviceWorker?.controller || navigator.serviceWorker?.active;

  worker.postMessage(`
    function doIt() {
      console.log('hello world');
    }
  `);
  
  new Worker('inject-fn.js');

you might need to append a query or something to make sure the browser doesn't load 'inject-fn.js' from the cache. As in

  new Worker(`inject-fn.js?${Math.random()});

The advantage to using a service-worker is from the POV of the page, the scripts come from the same URL where as via a blob they do not and so can not reference resources without absolute paths. Another problem with blobs is they are hard to debug because every time they're run them they'll get a random blob URL so any break points you set on the code in the blob will not match the next time.

The disadvantage to using a service-worker is they are finicky, at least in my experience.

Note: the code above is pseudo-code. It takes more code to properly start a service-worker and communicate with it. This site, for which the code is on github, and which is effectively a static web page, uses both techniques. The service worker when it can. The blob when it can not.

Upvotes: 1

Sirko
Sirko

Reputation: 74036

One option is to send the functions code through the usual channel and use the constructor new Function() (or eval()) to recreate the function.

In both cases you should check, what is actually transmitted to prevent security risks.

main script

// your function given as arguments and code
var funktion = {
  args: ['a', 'b' ],
  source: 'return a + b;'
};

// send it to your worker
worker.postMessage( funktion );

worker

self.addEventListener( 'message', function( msg ){

  // build array for constructor arguments
  var args = [ null ].concat( fk.a, fk.c );

  // create function
  var yourFunc = new (Function.prototype.bind.apply(Function, args));

  // use yourFunc
});

This uses the dynamic use of the Function constructor as described in this answer.


Using eval() may be simpler depending on how you have the function's code:

main script

// your function given as arguments and code
var funktion = "function add(a,b){ return a + b; }";

// send it to your worker
worker.postMessage( funktion );

worker

self.addEventListener( 'message', function( msg ){      

  // create function
  var yourFunc = eval( "(function(){ return " + funktion + "; })()" );

  // use yourFunc
});

Upvotes: 2

Related Questions