npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gramstax/worker-thread

v0.11.3

Published

Worker thread utilities with RPC and Unix socket gateway (Bun, Node.js).

Readme

@gramstax/worker-thread

Multi-runtime worker thread utilities with RPC and Unix socket gateway. Works in Bun and Node.js (>=18).

Features

  • Worker thread lifecycle management (WorkerThreadInit, WorkerThreadLaunch)
  • Type-safe RPC via proxy (WorkerThreadExport.init)
  • Inter-worker gateway over Unix domain sockets (WorkerThreadGatewayServer/Client)
  • Worker pool with broadcast (WorkerThreadPool)
  • JSON-RPC over node:worker_threads parentPort

Runtime Support

| Runtime | Status | Notes | | ------- | ------------ | ------------------- | | Bun | ✅ Supported | >= 1.0.0 — native | | Node.js | ✅ Supported | >= 18 — native |

Transport: In-worker IPC uses node:worker_threads. Inter-worker gateway uses Unix domain sockets via the net-ipc library, which works on both Bun and Node.js.

Dependencies: One runtime dep — net-ipc — for the Unix-socket gateway transport.

Install

bun add @gramstax/worker-thread
# or
npm install @gramstax/worker-thread

The Problem: Default node:worker_threads Has No Direct Worker-to-Worker Channel

node:worker_threads workers are isolated. Each worker has exactly one channel — its parentPort back to the thread that spawned it. There is no built-in way for two sibling workers to talk to each other; every message has to be relayed through the main thread.

flowchart TB
    subgraph vanilla["Vanilla node:worker_threads"]
        MT[Main Thread]
        W1[Worker A]
        W2[Worker B]
        W3[Worker C]
    end

    MT <-->|parentPort<br/>JSON-RPC| W1
    MT <-->|parentPort<br/>JSON-RPC| W2
    MT <-->|parentPort<br/>JSON-RPC| W3

    W1 -.->|❌ no direct channel| W2
    W2 -.->|❌ no direct channel| W3
    W1 -.->|❌ no direct channel| W3

Consequences:

  • Main thread becomes a forced message broker for all inter-worker traffic
  • Every cross-worker call costs two hops (sender → main → receiver) and two serializations
  • Main-thread event loop becomes a bottleneck — if it stalls, all worker-to-worker traffic stalls too
  • Adds latency and back-pressure on the central thread

The Solution: Unix-Socket Gateway Side-Channels

@gramstax/worker-thread adds a peer-to-peer transport on top of node:worker_threads. The transport is client–server under the hood (via net-ipc): one side calls createGatewayServer() to bind a socket, the other side calls createGatewayClient(path) to connect. The main thread is no longer required to relay messages between siblings.

Hub topology (most common): one worker runs the gateway server and exposes its methods; every other worker (and the main thread) connects to it as a client. No main-thread relay on the hot path.

flowchart TB
    subgraph solved["With @gramstax/worker-thread — hub topology"]
        MT[Main Thread<br/>WorkerThreadLaunch + Pool]
        W1[Worker A<br/>Init + GatewayClient]
        W2["Worker B<br/>Init + GatewayServer<br/>exposes mtdHello, mtdGetData, ..."]
        W3[Worker C<br/>Init + GatewayClient]
    end

    MT <-->|parentPort<br/>JSON-RPC| W1
    MT <-->|parentPort<br/>JSON-RPC| W2
    MT <-->|parentPort<br/>JSON-RPC| W3

    W1 -->|"Unix socket<br/>net-ipc<br/>(client → server)"| W2
    W3 -->|"Unix socket<br/>net-ipc<br/>(client → server)"| W2

Mesh topology (advanced): any worker can be both a server and a client at the same time — just call createGatewayServer() and createGatewayClient(peerPath) on the same instance. That gives you a full peer-to-peer mesh where every worker can expose methods to its peers and call into them.

flowchart TB
    subgraph mesh["With @gramstax/worker-thread — mesh topology"]
        W1["Worker A<br/>Init + Gateway(Server + Client)<br/>binds /tmp/A.sock<br/>connects to B and C"]
        W2["Worker B<br/>Init + Gateway(Server + Client)<br/>binds /tmp/B.sock<br/>connects to A and C"]
        W3["Worker C<br/>Init + Gateway(Server + Client)<br/>binds /tmp/C.sock<br/>connects to A and B"]
    end

    W1 <-->|Unix socket| W2
    W2 <-->|Unix socket| W3
    W1 <-->|Unix socket| W3

Important: a GatewayClient cannot talk directly to another GatewayClient — there is no discovery, no peer-to-peer mode in the transport. The socket is a named pipe with exactly one bind side and one or more connect sides. The "peer-to-peer" property of the package is that the main thread is not involved — not that any two endpoints can speak without a server somewhere.

Result:

  • Inter-worker calls are one hop, one serialization (no main-thread relay on the data path)
  • Main-thread is freed from relaying — it only orchestrates lifecycle (ready, kill)
  • Each worker can discover and connect to peers at runtime (via the peer's socket path)
  • Back-pressure on one channel does not block unrelated worker-to-worker traffic

Architecture Overview

flowchart LR
    subgraph Main["Main Thread"]
        Launch["WorkerThreadLaunch<br/>new Worker path, argv"]
        Pool["WorkerThreadPool<br/>thread: Map threadId → Launch"]
        Export["WorkerThreadExport.init<br/>Proxy + launchWorker + methods"]
    end

    subgraph WT1["Worker A"]
        Init1["WorkerThreadInit<br/>subclass with mtdFoo..."]
        GW1["WorkerThreadGatewayClient"]
    end

    subgraph WT2["Worker B"]
        Init2["WorkerThreadInit"]
        GW2["WorkerThreadGatewayServer"]
    end

    Export -->|launchWorker| Launch
    Pool -->|add/broadcastRequest| Launch
    Launch -->|new Worker| Init1
    Launch -->|new Worker| Init2

    Init1 <-->|parentPort| Launch
    Init2 <-->|parentPort| Launch

    Init1 -.->|createGatewayClient| GW1
    Init2 -.->|createGatewayServer| GW2
    GW1 <-->|Unix socket| GW2

Key components:

| Component | Lives in | Role | | --------------------------- | ------------- | ----------------------------------------------------------------------------- | | WorkerThreadLaunch | main thread | spawns a worker, holds parentPort link, exposes request() | | WorkerThreadInit | worker thread | base class for worker code; holds parentPort link, exposes request() | | WorkerThreadRpc | both | JSON-RPC dispatcher (request/result/error envelopes) | | WorkerThreadGatewayServer | worker thread | binds a Unix socket, routes incoming RPC calls to registered methods | | WorkerThreadGatewayClient | worker thread | connects to a peer's gateway socket, sends RPC requests | | WorkerThreadPool | main thread | manages many WorkerThreadLaunch instances, supports broadcast | | WorkerThreadExport.init | both | factory that returns a typed proxy whose methods auto-RPC across parentPort |

Lifecycle: Spawn → Ready → RPC → Kill

sequenceDiagram
    autonumber
    participant M as Main Thread
    participant L as WorkerThreadLaunch
    participant W as Worker (WTInit subclass)
    participant N as node:worker_threads

    M->>L: new WorkerThreadLaunch(path, argv)
    L->>N: new Worker(path, {argv})
    N->>W: spawn worker process
    W->>W: super() → init() → this.send({method: 'readyResolve'})
    W->>N: parentPort.postMessage({method: 'readyResolve'})
    N->>L: 'message' event
    L->>L: rpc.make({method: 'readyResolve'})<br/>→ register.readyResolve()
    L-->>M: _readyPromise resolves
    M->>M: await worker.ready() ✓

    M->>L: worker.request('mtdFoo', [arg1, arg2])
    L->>L: rpc.makeRequest → {id, method, params}
    L->>N: parentPort.postMessage(payload)
    N->>W: 'message' event
    W->>W: rpc.make({id, method, params})<br/>→ this['mtdFoo'](arg1, arg2)
    W-->>N: parentPort.postMessage({id, result})
    N-->>L: 'message' event
    L->>L: rpc.make({id, result}) → resolve promise
    L-->>M: returns the result

    M->>L: worker.kill()
    L->>N: thread.terminate()
    N-->>L: 'exit' event

Inter-Worker Communication (Direct, Bypassing Main Thread)

This is the headline feature. Once a worker has createGatewayServer() running, any other worker (or even the main thread) can reach its methods directly over a Unix socket — no relay, no main-thread bottleneck.

sequenceDiagram
    autonumber
    participant WA as Worker A
    participant GC as GatewayClient (in A)
    participant SOCK as Unix Socket
    participant GS as GatewayServer (in B)
    participant WB as Worker B
    participant M as Main Thread<br/>(only orchestrates)

    Note over WB,M: Phase 1: Server worker brings up gateway
    WB->>WB: this.createGatewayServer()<br/>(or in init/launchGatewayClient)
    WB->>GS: new WorkerThreadGatewayServer()
    GS->>SOCK: net-ipc.Server.start(path)<br/>bind + listen

    Note over WA,M: Phase 2: Client worker connects
    WA->>GC: new ServerBGateway(this).mtdBar(...)
    GC->>SOCK: net-ipc.Client.connect(path)
    SOCK-->>GS: 'connect' event
    GC->>GC: ensure client ready (lazily)
    GC->>SOCK: send {id, method: 'mtdBar', params: [...]}
    SOCK-->>GS: 'message' event
    GS->>WB: rpc.make({id, method, params})<br/>→ this.register['mtdBar'](...)
    WB-->>GS: returns result
    GS->>SOCK: send {id, result}
    SOCK-->>GC: 'message' event
    GC->>GC: rpc.make({id, result}) → resolve promise
    GC-->>WA: returns the result

    Note over M: Main thread was never on the path<br/>between WA and WB

Key points:

  • The main thread is completely off the inter-worker hot path — it only spawns/kills workers
  • The connection is lazily created on first call (owner.createGatewayClient(path) only runs if the gateway isn't already in owner.gatewayClients)
  • The register object can be this (expose the worker itself) or any plain object with callable methods
  • Multiple workers can share one gateway (server side fans out to all connected clients)
  • A worker can run both a server and clients — it can serve peers and call out to others

Pool Broadcast

WorkerThreadPool.broadcastRequest is the only "many-to-many" call that still uses the main-thread path — and that is by design: the main thread owns the worker registry, so fanning out from there is the natural place.

flowchart LR
    Pool[WorkerThreadPool<br/>broadcastRequest 'getStatus']
    L1[WorkerThreadLaunch 1]
    L2[WorkerThreadLaunch 2]
    L3[WorkerThreadLaunch 3]
    W1[Worker 1]
    W2[Worker 2]
    W3[Worker 3]

    Pool -->|request 'getStatus'| L1
    Pool -->|request 'getStatus'| L2
    Pool -->|request 'getStatus'| L3
    L1 -->|parentPort| W1
    L2 -->|parentPort| W2
    L3 -->|parentPort| W3
    W1 -.->|result| L1
    W2 -.->|result| L2
    W3 -.->|result| L3
    L1 -.-> Pool
    L2 -.-> Pool
    L3 -.-> Pool

If you need worker-to-worker broadcast (e.g. all workers need to ask every other worker the same question), wire it through a gateway — that path bypasses the pool and main thread entirely.

Quick Start

import { WorkerThreadExport, WorkerThreadInit } from "@gramstax/worker-thread";

class CounterWorker extends WorkerThreadInit {
  count = 0;
  mtdAutoIncrement(ms: string) {
    setInterval(() => {
      this.count += 1;
    }, ms);
  }

  mtdGetNumber() {
    return this.count;
  }
}

export const CounterJob = WorkerThreadExport.init({
  path: __filename,
  WTInit: CounterWorker,
});

// main.ts
await CounterJob.launchWorker();
console.log(await CounterJob.mtdGetNumber()); // 0
await CounterJob.mtdAutoIncrement(`1000`); // 1 seconds
setTimeout(() => {
  console.log(await CounterJob.mtdIncrement()); // >1
}, 2000);

Inter-worker gateway example

// server-worker.ts
import { WorkerThreadExport, WorkerThreadInit } from "@gramstax/worker-thread";

class ServerWorker extends WorkerThreadInit {
  static gatewayServerPath = "/tmp/server.sock";
  mtdHello(from: string) {
    return `Hello ${from}!`;
  }

  async mtdStart() {
    return await this.createGatewayServer();
  }
}

export const Server = WorkerThreadExport.init({
  path: __filename,
  WTInit: ServerWorker,
});
export const ServerGateway = Server.launchGatewayClient();

// client-worker.ts
import { ServerGateway } from "./server-worker";
import { WorkerThreadExport, WorkerThreadInit } from "@gramstax/worker-thread";

class ClientWorker extends WorkerThreadInit {
  async mtdCallServer() {
    // pass `this` as first arg — the gateway client needs an owner with a gatewayClients map
    return await ServerGateway.mtdHello(this, "ClientWorker");
  }
}

export const Client = WorkerThreadExport.init({
  path: __filename,
  WTInit: ClientWorker,
});

// main.ts
await Server.launchWorker();
await Client.launchWorker();
await Server.mtdStart();
console.log(await Client.mtdCallServer()); // "Hello ClientWorker!"

API

| Export | Description | | --------------------------- | -------------------------------------------- | | WorkerThreadInit | Base class for worker thread implementations | | WorkerThreadLaunch | Main-thread side to spawn workers | | WorkerThreadExport | init() factory creating typed proxy | | WorkerThreadPool | Manage multiple workers + broadcast | | WorkerThreadRpc | JSON-RPC dispatcher | | WorkerThreadGatewayServer | Unix-socket server for inter-worker comms | | WorkerThreadGatewayClient | Unix-socket client for inter-worker comms |

See JSDoc in source for full API.

License

Proprietary — Copyright (c) 2026 Gramstax. See LICENSE.