Nanolith
Multithreading in Node.js made simple with seamless TypeScript support.
Install / Use
/learn @mstephen19/NanolithREADME
Nanolith
<center> <img src="https://user-images.githubusercontent.com/87805115/217611158-14822948-f312-4fb6-af0e-83d534ce854f.png" width="550"> </center>More intuitive and feature-rich than Piscina!
❔ About
✨Nanolith✨ is a scalable, reliable, easy-to-use, and well-documented multithreading library that allows you to easily vertically scale your Node.js applications. Based on worker_threads, it serves to not only build upon, but entirely replace the (deprecated) Threadz library.
There have always been a few main goals for Nanolith:
- Performance & scalability 🏃
- Ease-of-use 😇
- Seamless TypeScript support 😎
- Modern ESModules support 📈
- Steady updates with new features & fixes 🚀
So what can you do with it?
Here's a quick rundown of everything you can do in Nanolith:
- Offload expensive tasks to separate threads.
- Spawn separate-threaded "nanoservices" that can run any tasks you want.
- Communicate back and forth between threads through events.
- Stream data between threads with the already familiar
node:streamAPI. - Share memory between threads using the familiar-feeling
SharedMap.
📖 Table of contents
- ❔ About
- 💾 Installation
- 📝 Defining your tasks
- 👷 Running a task
- 🎩 Understanding services
- 🎬 Coordinating services
- 🪝 Hooks
- 🚨 Managing concurrency
- 📨 Communicating between threads
- 📡 Streaming data between threads
- 💾 Sharing memory between threads
- 📜 License
💾 Installation
The latest version can be installed via any package manager of your choice.
npm install nanolith@latest
# or
yarn add nanolith@latest
Beta versions are released under the next tag and can be installed like this:
npm install nanolith@next
# or
yarn add nanolith@next
📝 Defining your tasks
A task is any function that you define which is accessible by Nanolith's APIs. Tasks can be defined using the define() function in a separate file dedicated to definitions.
// worker.ts 💼
import { define } from 'nanolith';
// Exporting the variable is not a requirement, but it is
// necessary to somehow export the resolved value of the
// function in order to have access to it later on.
export const worker = await define({
add(x: number, y: number) {
return x + y;
},
async waitThenAdd(x: number, y: number) {
await new Promise((resolve) => setTimeout(resolve, 5e3))
return x + y;
},
// Functions don't have to be directly defined within the
// object, they can be defined elsewhere outside, or even
// imported from a totally different module.
subtract,
});
function subtract(x: number, y: number) {
return x - y;
};
By passing functions into define(), you immediately turn them into multithreadable tasks. No further configuration is required.
define() options
As seen above, the first argument to define() is an object containing your functions. The second parameter is an object accepting the following (optional) configurations:
| Name | Type | About |
|-|-|-|
| file | string | If define()'s file location detection is not working correctly, the true file location for the set of definitions can be provided here. |
| identifier | string | A unique identifier for the set of definitions. Overrides the auto-identifier generated by Nanolith. |
| safeMode | boolean | Whether or not to prevent the usage of the returned Nanolith API from within the same file where their definitions were created. Defaults to true. |
👷 Running a task
After defining a set of tasks, you can import them and call them anywhere by directly using the Nanolith API resolved by the define() function. The only difference is that instead of being called on the main/parent thread, a new thread will be created for the task and it will be run there.
// 💡 index.ts
// Importing the Nanolith API we created in worker.ts
import { worker } from './worker.js';
// Run the "add" function on a separate thread and wait
// for it to complete before moving forward.
const result = await worker({
// Provide the name of the task.
name: 'add',
// Provide the parameters of the function.
params: [2, 3],
});
// The result is sent back to the parent thread
// and resolved by the task function call.
console.log(result); // -> 5
The new thread's process is shut down after the task finishes.
📝 Note: Notice that even with the synchronous
add()function, it is now asynchronous when being run on a child thread.
Task function options
name and params are amongst many of the possible options that can be passed in when running a task:
| Name | Type | About |
|-|-|-|
| name | string | The name of the task to call. Must be present on the set of definitions. |
| params | any[] | The arguments for the task in array form. |
| priority | boolean | Whether or not to treat the task's worker as priority over others when being queued into the pool. Defaults to false. |
| reffed | boolean | When true, the underlying Worker instance is reffed. Defaults to true. |
| sharedEnv | boolean | Whether or not to shared environment variables between the parent thread (current) and the child thread to be created. Defaults to true. |
| messengers | Messenger[] | The Messengers that should be accessible to the task. |
| options | object | An object containing most of the options available on the Worker constructor. |
🎩 Understanding services
Services are Nanolith's flagship feature. Running a task on a service works similarly to running a task normally; however, the key difference is that the thread only shuts down when you tell it to. This means that you can run multiple tasks on the same thread rather than spawning up a new one for each call, which is where the real benefits of multithreading in Node.js can be seen.
Considering the definitions we created here, here is how a service would be launched and a task would be called on it.
// 💡 index.ts
// Importing the Nanolith API we created in worker.ts
import { worker } from './worker.js';
// Spawn up a new thread that has access to all of
// our tasks.
const service = await worker.launchService();
// Command the service thread to run the "add" function.
const result = await service.call({
name: 'waitThenAdd',
params: [2, 3],
});
// We can run service.call() as many times as we want, and
// all those tasks will be called on the same thread...
// Similarly to regular task calls, the return value
// is sent back to the parent thread and resolve by the call.
console.log(result);
// Shut down the second thread.
await service.close();
launchService() options
The configurations for Nanolith.launchService() are nearly identical to the task function options with the addition of exceptionHandler:
| Name | Type | About |
|-|-|-|
| exceptionHandler | function | An optional but highly recommended option that allows you to catch uncaught exceptions within the service. |
| priority | boolean | Whether or not to treat the service's worker as priority over others when being queued into the pool. Defaults to false. |
| reffed | boolean | When true, the underlying Worker instance is reffed. Defaults to true. |
| sharedEnv | boolean | Whether or not to shared environment variables between the parent thread (current) and the child thread to be created. Defaults to true. |
| messengers | Messenger[] | The Messengers that should be accessible to the service. |
| options | object | An object containing most of the options available on
