Node Worker Threads Pool for CPU Tasks

Offload CPU-bound work to a pool of worker threads with Piscina-style sizing, transferable buffers, and graceful pool shutdown.

JavascriptNodeResilience

Code

snippet
import { Worker } from 'node:worker_threads';
import { mkdtemp, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

const WORKER_SOURCE = `
const { parentPort, workerData } = require('node:worker_threads');
function fib(n) { return n < 2 ? n : fib(n - 1) + fib(n - 2); }
parentPort.postMessage({ input: workerData.n, result: fib(workerData.n) });
`;

class WorkerPool {
  constructor(size) {
    this.size = size;
    this.workers = [];
    this.queue = [];
    this.busy = new Set();
  }

  async init() {
    const dir = await mkdtemp(join(tmpdir(), 'pool-'));
    this.file = join(dir, 'worker.js');
    await writeFile(this.file, WORKER_SOURCE, 'utf8');
    for (let i = 0; i < this.size; i++) this.workers.push(new Worker(this.file));
  }

  run(n) {
    return new Promise((resolve, reject) => {
      this.queue.push({ n, resolve, reject });
      this.dispatch();
    });
  }

  dispatch() {
    while (this.queue.length > 0 && this.busy.size < this.size) {
      const job = this.queue.shift();
      const worker = this.workers.find((w) => !this.busy.has(w));
      this.busy.add(worker);
      worker.once('message', (msg) => {
        this.busy.delete(worker);
        job.resolve(msg.result);
        this.dispatch();
      });
      worker.postMessage({ n: job.n });
    }
  }

  async destroy() {
    await Promise.all(this.workers.map((w) => w.terminate()));
  }
}

const pool = new WorkerPool(4);
await pool.init();
console.log(await pool.run(35));
await pool.destroy();