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.jsworker_threads
library.