Skip to content

Workers

Pranav edited this page Aug 11, 2023 · 2 revisions

Workers

This is most likely the hardest part of the app to understand. In JS, a worker is a way of multithreading your code, allowing you to separate costly tasks from the main UI thread. This way the app won’t freeze every time you try searching a database. It is different from an async function, which still runs in the main thread.

Workers can’t access the UI thread, so you won’t be able to access the DOM from a function within the "worker.js" file. Here is a (simplified) diagram of how it works.

Untitled Diagram drawio

A couple notes:

A worker is NOT an asyncronous function. A worker runs parallel to the main thread, so worker code is executed at the same time as the code in the main thread. As a result you can't use await on a worker, and instead must supply a function to the callback parameter.

A more thorough explanation:

When should we use a worker?

You can use web workers to complete resource intensive tasks separate from your main thread. This way, you can prevent your UI from being slow or unresponsive, as you are essentially running tasks in parallel. The most common scenario in which web workers are used is when you need to update a large database from an external server, or you need to search a database for a certain entry. Asyncronous functions are not truly parallel since they still run on the same thread as your UI, so they will still slow down your app.

Creating the worker:

Workers are created by first initializing a WorkerHandler object (view "common.js"). This object allows you to create a worker and handle "events" that are "dispatched" by the worker. Example initialization: let worker = new WorkerHandler();. Once the WorkerHandler has been initialized, a new "Web Worker" is spawned. For more info on web workers, click here.

Using the worker:

To make the worker do stuff, you will need to use the .work() method. This method has two parameters: the first is an array, with the first element being a string equal to the name of the worker function you wish to execute; the second is parameter is a function, also known as a "callback".

Example:

let worker = new WorkerHandler();
worker.work(['function_one', 'additonalParams'], printStatus);
function printStatus(message){
    console.log(message);
}

Once worker.work(['function_one', 'additonalParams'], printStatus) is called, the first parameter (the array) is sent (or "posted") to the Web Worker, who's code is located in worker.js. In worker.js, the first element in the array is passed into a switch, and the corresponding code is executed. Suppose this is the code in worker.js:

...
onmessage = function(e){ // this function is called when a message is sent ("posted") to the worker. e is the array that was sent to the worker.
    switch (e.data[0]) {
        case 'function_one': // This is the code that will be executed
            console.log(e.data[1]);
            postMessage(['result', 'success']); // this function sends ("posts") a message back to the creator of the worker (the WorkerHandler)
            break;
        case 'function_two':
            doSomething(e.data[1], e.data[2]).then((result) => {
                postMessage(['result', 'finished', result]);
            }
            break;
        case 'function_three':
......etc.

We can see that passing 'function_one' into the switch will print the second element in the array that was passed to the worker. In the example, this means console.log('additionalParams') will be executed by the worker.

Afterwards, we can see that the worker calls postMessage(['result', 'success']);. Essentially, this dispatches an event to the WorkerHandler. The postMessage function can only called from worker.js and requires an array as a parameter. The first element of this array is the "category" of the event. All elements succeeding the first element depend upon the category. The WorkerHandler definition is located in "common.js". Suppose this is the code in common.js:

class WorkerHandler {
    async work(params, callback) {
    const worker = new Worker('./worker.js');
    worker.postMessage(params);
    worker.onmessage = (e) => {
        if (e.data[0] === 'result') {// This is the code that will be executed
            worker.terminate(); // Terminates the worker thread. After this, the worker will not execute any more code and is freed from memory.
            callback(e.data.slice(1,)); // Calls the callback function (second parameter of worker.work) with all elements after the first being passed as an argument
        } else if (e.data[0] === 'error') {
            console.error(e.data[1]);
            worker.terminate();
        } else if (e.data[0] === 'progress') {
            let bar = new ProgressBar();
            log.info(e.data[2]);
    ......etc.

We can see that if the first element is equal to 'result', worker.terminate is called. This kills the worker thread, and the worker will stop execution of code, even if it hasn't finished completing the function. Afterwards, callback(e.data.slice(1,)) is called. e.data.slice(1,) simply returns array e with the first element removed. Earlier, callback was defined as printStatus, and we know e is equal to ['result', 'success']. Therefore, the following function is executed: printStatus(['success']);

Overall the expected output for our example is additionalParams ['success']