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

@tachyon-ipc/core

v0.4.2

Published

Tachyon IPC bindings for Node.js

Readme

Tachyon Node.js bindings

npm

Node.js bindings for Tachyon, a bare-metal lock-free IPC. SPSC ring buffer over POSIX shared memory with sub-100 ns p50 RTT.

The C++ core is exposed via N-API v8. Prebuilds are provided for Linux x64. On other platforms the addon is compiled from source at installation time via cmake-js.



Requirements

| Component | Minimum | |------------|-------------------------------------------| | OS | Linux 5.10+ (primary), macOS 13+ (tier-2) | | Node.js | 20+ | | TypeScript | 5.2+ (using keyword / ES2023 ERM) | | Compiler | GCC 14+ or Clang 17+ (source builds only) |

Install

npm install @tachyon-ipc/core

Prebuilds for Linux x64 are bundled. On other platforms, cmake-js compiles the addon from source. GCC 14+ or Clang 17+ must be available on the build machine.

The package ships as ESM ("type": "module"). CommonJS consumers must use dynamic import().

Quickstart

The consumer must start first, it owns the UNIX socket and the SHM arena.

// worker.ts - consumer side (must run on a Worker thread)
import {Bus} from '@tachyon-ipc/core';

using bus = Bus.listen('/tmp/demo.sock', 1 << 16);
const {data, typeId} = bus.recv();
console.log(`received ${data.byteLength} bytes, type_id=${typeId}`);
// producer - main thread or any Worker
import {Bus} from '@tachyon-ipc/core';

using bus = Bus.connect('/tmp/demo.sock');
bus.send(Buffer.from('hello tachyon'), 1);

Bus implements Disposable. The using keyword (TypeScript 5.2+, ES2023 Explicit Resource Management) guarantees close() is called on scope exit regardless of exceptions.

API

Lifecycle

Bus.listen(socketPath, capacity) creates a new SHM arena and binds a UNIX socket. This call blocks the calling thread until exactly one producer calls connect. Always invoke it from a Worker thread to avoid stalling the main event loop. capacity must be a strictly positive power of two.

Bus.connect(socketPath) attaches to an existing arena. Returns immediately after the handshake.

bus.close() unmaps shared memory. Safe to call multiple times. Prefer using.

bus.flush() publishes pending unflushed TX messages. bus.send() and tx.commit() call it internally.

Standard API

bus.send(data: Buffer | Uint8Array, typeId?: number) copies the payload into the ring buffer, commits, and flushes. typeId defaults to 0.

bus.recv(spinThreshold?: number): { data: Buffer; typeId: number } blocks until a message is available. Returns a heap-owned Buffer, safe to retain indefinitely. Retries transparently on EINTR.

Zero-copy TX

bus.acquireTx(maxSize: number): TxGuard acquires a TX slot. Write into the slot via TxGuard.bytes(), which returns a TxSlot (branded Buffer) pointing directly into shared memory. Finalize with one of:

  • tx.commit(actualSize, typeId): publish and flush.
  • tx.commitUnflushed(actualSize, typeId): publish without flushing; call bus.flush() after the batch.
  • tx.rollback(): cancel without publishing.

TxGuard implements Disposable. Exiting a using block without an explicit commit triggers automatic rollback, preventing indefinite producer lock starvation.

using tx = bus.acquireTx(64);
payload.copy(tx.bytes());
tx.commit(payload.byteLength, 7);

Zero-copy RX

bus.acquireRx(spinThreshold?: number): RxGuard | null blocks until a message is available. Returns null on EINTR, the caller decides whether to retry. Read via rx.data(), rx.typeId, and rx.actualSize, then commit.

RxGuard implements Disposable. Exiting a using block without an explicit commit triggers automatic commit.

const rx = bus.acquireRx();
if (rx === null) return; // EINTR
using _ = rx;
process(rx.data(), rx.typeId);
// commits on scope exit

Batch RX

bus.drainBatch(maxMsgs: number, spinThreshold?: number): RxBatch blocks until at least one message is available, then drains up to maxMsgs in a single N-API crossing. Use batch.at(i) for indexed access or for...of for iteration. Call batch.commit() when done.

RxBatch implements Disposable. Exiting a using block triggers automatic commit.

using batch = bus.drainBatch(64);
for (const msg of batch) {
    process(msg.data, msg.typeId, msg.size);
}
// auto-commits on scope exit - all ArrayBuffers are detached

Zero-copy pattern

tx.bytes() and rx.data() return Buffer objects backed by a noop_finalizer, the N-API layer does not copy and does not free the underlying shared memory. They are valid only until the corresponding commit(), commitUnflushed(), or rollback() call.

On batch.commit(), the C++ layer detaches every ArrayBuffer backing the batch slots. Any cached reference will immediately throw TypeError: Cannot perform %TypedArray%.prototype.set on a detached ArrayBuffer, a fail-fast enforced at the V8 level, with zero SHM writes and zero cache invalidation.

// Safe: copy before the guard closes.
using rx = bus.acquireRx()!;
const snapshot = Buffer.from(rx.data()); // heap copy
// rx.data() would throw after scope exit
process(snapshot); // valid indefinitely

Batch pattern

// Batch TX - one flush for N messages.
for (const payload of payloads) {
    using tx = bus.acquireTx(payload.byteLength);
    payload.copy(tx.bytes());
    tx.commitUnflushed(payload.byteLength, typeId);
}
bus.flush();
// Batch RX - one N-API crossing for up to 64 messages.
using batch = bus.drainBatch(64);
for (const msg of batch) {
    process(msg.data, msg.typeId);
}
// auto-commits; all msg.data ArrayBuffers are detached

drainBatch crosses the N-API boundary exactly once per call. Draining 64 messages amortizes the ~15–20 ns per-crossing overhead by 64×.

Worker threads

Bus.listen(), bus.recv(), bus.acquireRx(), and bus.drainBatch() are synchronous blocking C calls. Invoking them on the main thread blocks the V8 event loop for their full duration (timers), I/O callbacks, and GC do not run.

Always run the consumer on a dedicated Worker thread:

// consumer-worker.ts
import {parentPort} from 'node:worker_threads';
import {Bus} from '@tachyon-ipc/core';

using bus = Bus.listen('/tmp/bus.sock', 1 << 16);
parentPort!.postMessage({type: 'ready'});

for (; ;) {
    const {data, typeId} = bus.recv();
    parentPort!.postMessage({data, typeId}, [data.buffer]);
}
// main.ts
import {Worker} from 'node:worker_threads';
import {createRequire} from 'node:module';
import {Bus} from '@tachyon-ipc/core';

const TSX_ESM = createRequire(import.meta.url).resolve('tsx/esm');
const worker = new Worker(new URL('./consumer-worker.ts', import.meta.url), {
    execArgv: ['--import', TSX_ESM],
});

using bus = Bus.connect('/tmp/bus.sock');
bus.send(Buffer.from('ping'), 1);

Native addon handles (tachyon_bus_t*) cannot cross Worker thread boundaries, each side must hold its own Bus instance.

Branded types

TxSlot and RxSlot are nominal subtypes of Buffer:

declare const txSlotBrand: unique symbol;
declare const rxSlotBrand: unique symbol;

export type TxSlot = Buffer & { readonly [txSlotBrand]: true };
export type RxSlot = Buffer & { readonly [rxSlotBrand]: true };

The brand symbols are module-private. TxSlot can only be produced by TxGuard.bytes(); RxSlot only by RxGuard.data() or RxMessage.data. This makes it a compile-time contract: TypeScript's structural type system cannot enforce the distinction between an arbitrary Buffer and a live SHM-backed slot (the brand does).

Error handling

import {isAbiMismatch, isFull, isPeerDead, isTachyonError} from '@tachyon-ipc/core';

try {
    using bus = Bus.connect('/tmp/demo.sock');
    bus.send(payload, 1);
} catch (err) {
    if (isAbiMismatch(err)) {
        throw new Error('Rebuild producer and consumer from the same Tachyon tag.');
    }
    if (isFull(err)) {
        // Ring buffer full - producer outpacing consumer.
    }
    if (isPeerDead(err)) {
        // Bus entered FATAL_ERROR - corrupted message header. Close immediately.
    }
    throw err;
}

| Guard | Matches | |--------------------|---------------------------------------------------------------------| | isTachyonError() | Any error from the native binding (carries an ERR_TACHYON_* code) | | isAbiMismatch() | Handshake rejected - TACHYON_MSG_ALIGNMENT mismatch | | isFull() | Ring buffer full - ERR_TACHYON_FULL | | isPeerDead() | Bus entered TACHYON_STATE_FATAL_ERROR (corrupted message header) |

PeerDeadError is raised by the TypeScript layer (not the native binding) after polling tachyon_get_state(). It carries code ERR_TACHYON_UNKNOWN.

Thread safety

Bus is not thread-safe. Each direction (TX or RX) must be used by at most one thread at a time. Tachyon is SPSC, not MPSC.

A Worker thread that blocks inside a Tachyon call parks its OS thread for the duration of the call. N consumers on N Worker threads result in N parked OS threads. This is expected and bounded.

NUMA binding

using bus = Bus.listen(path, 1 << 16);
bus.setNumaNode(0); // bind SHM pages to NUMA node 0, before the first message

setNumaNode uses MPOL_PREFERRED + MPOL_MF_MOVE. No-op on macOS.

Prebuild vs. Compile

The package ships a prebuilt .node addon for Linux x64 (glibc 2.31+). The addon is loaded at runtime via createRequire from build/Release/tachyon_node.node or build/Debug/tachyon_node.node.

To force a source build:

npm install @tachyon-ipc/core --build-from-source

To rebuild after modifying the C++ core:

rm -rf build && npm run build:native

Type ID encoding

typeId is a number (uint32) split into two 16-bit halves since v0.4.0:

import {makeTypeId, routeId, msgType} from '@tachyon-ipc/core';

const id = makeTypeId(0, 42); // == 42, identical to v0.3.x
routeId(id); // 0
msgType(id); // 42

bus.send(data, makeTypeId(0, 42));

const {typeId} = bus.recv();
msgType(typeId); // 42

routeId >= 1 is reserved for RPC. Do not use it on consumers.

Limitations

Node.js 20+ required. import.meta.dirname (used for addon path resolution) is available from Node 21.2+; on Node 20 pass --experimental-import-meta-resolve.

Blocking calls must run on Worker threads. Bus.listen(), recv(), acquireRx(), and drainBatch() are synchronous C calls that block the calling OS thread.

ESM only. The package ships with "type": "module". CommonJS consumers must use import().

Linux is the primary platform. setNumaNode, futex-based sleep, and memfd_create are Linux-specific.

SPSC only. One producer, one consumer.

No peer crash detection. If the producer crashes, blocking consumer calls stalls indefinitely.

License

Apache 2.0