Workers
Overview
Workers are a way to run code in the background, without blocking the main thread. They are useful for running tasks that are not time-sensitive, such as sending analytics data, or processing images, or even doing computational work.
In Sodacore workers are implemented as "Worker Providers", which are classes that act as a Provider, can be injected, etc, but when called they will actually hand off the method to a Bun worker to run in the background.
Under the hood, the main logic is very simple, we implement a WorkerWrapper class, that simply handles IPC communication between the worker and the main thread, you then inject your worker class, and call methods against it as normal, and the wrapper will translate that as a message to the worker to process that, and then return the result, we use a simple queue system to hold resolve/reject's to then pass back on return of the method result.
A small note, you may want to utilise the
isMainThread
boolean, that is exposed within thebun
package, so that you can enforce that certain code runs only in the worker thread.
Prerequisites
To create a worker, there are a few things we need to be aware of, the file path, but also to verify that Bun has compiled our file separately, so that it can be called in to the worker wrapper.
To build the application, by default, Bun will bundle to a single file, which is great for speed, but not so great for workers, so we need to ensure that we are building the worker files separately, to do this, we need to add a new entry to the bun.config.ts
file, like so:
bun build src/main.ts src/worker/yourworker.ts --outdir ./dist --target=bun
As you can see we tell Bun to compile the src/main.ts
file which will bundle to a single file, but then we tell Bun to compile the src/worker/yourworker.ts
file separately as well, you can do this for each worker to ensure they compile separately.
An alternative way is to tell Bun to compile all files separately, this will lead to duplication of code in the actual output folder, but it won't have a performance loss, because the actual code that is ran will be from the single executable that Bun created, I would say, it's not advisable, but I do it for the core
package, so 🤷, who am I to tell you not to do it 😆, so here is how to do that:
bun build src/main.ts src/**/*.ts --outdir ./dist --target=bun
Usage
To create a worker, you will need to be aware of the filepath you are loading, fortunately the core package has some useful helpers to make this easier, as the built-in resolve method resolves to the module, regardless of the path you give it, so you can use it to resolve the worker file, like so:
Note we have a helper than automatically checks the
process.env.NODE_ENV
automatically for, so you just need to put a relative path from the output file bundle, so if your output is./dist/main.js
then you would do:../worker/yourworker.ts
to reference your worker file.
import { Worker, Utils, Expose } from '@sodacore/core';
@Worker(Utils.getFilePath('../worker/example.ts'))
export class ExampleWorker {
@Expose()
public async exampleMethod() {
return 'Hello from the worker!';
}
}
The above code does the following:
- We create a new class called
ExampleWorker
. - We use the
@Worker
decorator to tell the framework that this is a worker. - We define our path within the worker, using the
Utils.getFilePath
helper, as in dev mode, all files are loaded separately and thereforeimport.meta.filepath
is correct, but in production, all files are bundled into a single file, so we need to resolve the path to the worker file. - We create a method called
exampleMethod
and use the@Expose
decorator to tell the framework that this method is a "worker method" and should proxy this to the worker.
Caveats
Context is lost between the main thread and the worker thread, the Registry will not be populated, and you will not be able to pass in resource-based data like database connections, etc, you will need to initialise these from the worker itself, by default.
Workers have no access to application lifecycle events, by default they come with a
init
method though, so if you define a method calledinit
it will be called when the worker is initialised automatically, it is possible to call lifecycle events against an exposed worker method, but I am unsure if the decorator will trigger the proxied method (which it should), worth trying though; let me know.