@joystick.js/node

Workers

How to use Node.js workers to offload CPU-heavy tasks in a Joystick app.

In Node.js, worker threads are a way to offload CPU-intensive tasks from the main execution thread. A worker thread runs a standalone Node.js script that performs some task in your app (e.g., crunching some numbers), but does so in a way that doesn't block the main thread.

The result is that your main execution thread (e.g., your Joystick app handling inbound HTTP requests) is not blocked by compute-heavy work.

As a convenience, Joystick ships with a simple abstraction for defining and calling workers to help with scaling tasks in your app.

Example Usage

Defining a worker

In your app, all worker scripts live as standalone .js JavaScript files in the /workers folder at the root of your app.

workers/example.js

import { parentPort as parent_port, workerData as worker_data } from 'worker_threads';

const transactions = worker_data?.transactions || [];
const total = transactions?.reduce((total = 0, transaction = {}) => {
  total += transaction?.total;
  return total;
});

parent_port.postMessage(total);
Worker must call parent_port.postMessage()

Whether you're returning a value or just signaling success, in order for workers to behave properly, you will need to call parent_port.postMessage(). This message is the response value to your original worker() call. If you omit this, workers can get "stuck" in a loop because they're never resolved (Joystick considers a worker resolved when it sends a message).

Above, we've defined a simple worker that ingests an array of transactions and then totals them up, returning the result.

To accomplish this, we use the native-Node.js worker_threads library, importing parentPort as parent_port and workerData as worker_data (this isn't required, just done as an example here to keep code-styling consistent).

From worker_data, we reference the value transactions which we assume will be passed when our worker is invoked. Once we've calculated our total, to send the result back to the invocation (remember, this is technically a standalone Node.js script, not a traditional function), we call to parent_port.postMessage(), passing our total.

Calling a worker

To call our example worker, in our main application code, we can import the worker method that's exported from @joystick.js/node.

index.server.js

import joystick, { worker } from '@joystick.js/node';

joystick.app({
 ...
}).then(() => {
  const total = await worker('example', { transactions: [/* Array of transaction objects... */]});
  // Do something with total here...
});

Above, as an example, we import the named export worker from @joystick.js/node and then after our server starts up, we call it, passing the name of the worker file we want to run (e.g., we pass example as our file is called example.js—if our file were called make_pizza.js, we'd pass make_pizza here).

Additionally, we pass our transactions as an object. Here, the object we pass as the second argument to worker() will be mapped to the worker_data value we referenced when defining our worker above.

Once our worker runs, because we called to parent_port.postMessage() internally, we expect the value we pass to that (our total) to be returned from the worker() call (Joystick abstracts this internally as a convenience so we can call a worker similar to a normal function).

API

worker()

worker(worker_file_name: string, worker_data: object) => Promise

Parameters

worker_file_name string required
The name of the worker file in your workers directory that you'd like to run (should just be the file name minus the .js extension).
worker_data object
Data that you'd like to pass to your worker. Inside of the worker, maps to the workerData value provided by the Node.js worker_threads library.