🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@nxtedition/shared

Package Overview
Dependencies
Maintainers
10
Versions
81
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@nxtedition/shared

A high-performance, thread-safe ring buffer for inter-thread communication in Node.js using `SharedArrayBuffer`.

npmnpm
Version
5.0.1
Version published
Weekly downloads
2.4K
65.28%
Maintainers
10
Weekly downloads
 
Created
Source

@nxtedition/shared

A high-performance, thread-safe ring buffer for inter-thread communication in Node.js using SharedArrayBuffer.

Why

Passing data between worker threads in Node.js typically involves structured cloning or transferring ArrayBuffer ownership. Structured cloning copies every byte — fine for occasional messages, but a bottleneck when streaming megabytes per second between threads. Transferable objects avoid the copy, but each transfer still requires allocating a new ArrayBuffer, serializing the transfer list, and coordinating ownership between threads — overhead that adds up quickly in high-throughput scenarios.

This ring buffer avoids these problems. A single SharedArrayBuffer is mapped into both threads. The writer appends messages by advancing a write pointer; the reader consumes them by advancing a read pointer. No copies, no ownership transfers, no cloning overhead. Because messages are stored inline in a contiguous buffer, data lives right where the protocol is rather than scattered across separately allocated ArrayBuffers — keeping access patterns cache-friendly. The pointers are coordinated with Atomics operations, and cache-line-aligned to prevent false sharing between CPU cores.

Reads are zero-copy: the reader callback receives a DataView directly into the shared buffer, so parsing can happen in-place without allocating intermediate buffers. Writes are batched — the write pointer is only published to the reader after a high-water mark is reached or the current event loop tick ends, drastically reducing the frequency of expensive atomic stores.

Platform Assumptions

All messages are aligned on 4-byte boundaries. Message length headers are read and written via Int32Array indexing rather than DataView, avoiding per-access endianness checks on the hot path.

Install

npm install @nxtedition/shared

Usage

import { alloc, Reader, Writer } from '@nxtedition/shared'

// Allocate shared memory (pass handle to a worker thread)
const state = alloc(1024 * 1024) // 1 MB ring buffer

// --- Writer side (e.g. main thread) ---
const w = new Writer(state)

const payload = Buffer.from('hello world')
w.writeSync(payload.length, (data) => {
  payload.copy(data.buffer, data.byteOffset)
  return data.byteOffset + payload.length
})

// --- Reader side (e.g. worker thread) ---
const r = new Reader(state)

r.readSome((data) => {
  const msg = data.buffer.subarray(data.byteOffset, data.byteOffset + data.byteLength).toString()
  console.log(msg) // 'hello world'
})

Batching writes with cork

w.cork(() => {
  for (const item of items) {
    const buf = Buffer.from(JSON.stringify(item))
    w.writeSync(buf.length, (data) => {
      buf.copy(data.buffer, data.byteOffset)
      return data.byteOffset + buf.length
    })
  }
})
// All writes flushed atomically when cork returns

Or manually:

w.cork()
w.writeSync(buf1.length, writeFn, buf1)
w.writeSync(buf2.length, writeFn, buf2)
w.uncork() // publishes all writes to the reader

Non-blocking writes with tryWrite

const buf = Buffer.from('data')
const ok = w.tryWrite(buf.length, (data) => {
  buf.copy(data.buffer, data.byteOffset)
  return data.byteOffset + buf.length
})
if (!ok) {
  // Buffer is full — the reader hasn't caught up yet
}

Cross-thread usage

// main.js
import { alloc, Writer } from '@nxtedition/shared'
import { Worker } from 'node:worker_threads'

const state = alloc(1024 * 1024)
const worker = new Worker('./reader-worker.js', {
  workerData: state,
})

const w = new Writer(state)
// ... write messages
// reader-worker.js
import { Reader } from '@nxtedition/shared'
import { workerData } from 'node:worker_threads'

const r = new Reader(workerData)

function poll() {
  const count = r.readSome((data) => {
    // process data.buffer at data.byteOffset..data.byteOffset+data.byteLength
  })
  setImmediate(poll)
}
poll()

API

alloc(size)

Allocates a shared ring buffer. Returns a handle that can be passed directly to Reader, Writer, and worker threads via workerData.

  • size — Maximum payload size in bytes for a single write (must be a positive integer, max ~2 GB)

Overhead (state header + length header + alignment) is added automatically on top of size.

new Reader(handle)

Creates a reader for the ring buffer.

reader.readSome(next, opaque?)

Reads a batch of messages. Calls next(data, opaque) for each message, where data has:

  • buffer: Buffer — The underlying shared buffer

  • view: DataView — A DataView over the shared buffer

  • byteOffset: number — Start offset of the message payload

  • byteLength: number — Length of the message payload in bytes

  • opaque — Optional user-provided context object passed through to the callback. Useful for avoiding closures on hot paths.

Return false from the callback to stop reading early. Returns the number of messages processed.

Messages are batched: up to 1024 items or 256 KiB per call.

new Writer(handle, options?)

Creates a writer for the ring buffer.

Options:

  • yield?: () => void — Called when the writer must wait for the reader to catch up. Useful to prevent deadlocks when the writer thread also drives the reader.
  • logger?: { warn(obj, msg): void } — Logger for yield warnings (pino-compatible).

writer.writeSync(len, fn, opaque?)

Synchronously writes a message. Blocks (via Atomics.wait) until buffer space is available.

  • len — Maximum payload size in bytes. Writing beyond len bytes in the callback is undefined behavior.
  • fn(data, opaque) → number — Write callback. Write payload into data.buffer starting at data.byteOffset. Must return the end position (data.byteOffset + bytesWritten), not the byte count.
  • opaque — Optional user-provided context object passed through to the callback. Useful for avoiding closures on hot paths.

Throws on timeout (default: 60000 ms).

writer.tryWrite(len, fn, opaque?)

Non-blocking write attempt. Returns false if the buffer is full. The fn and opaque parameters follow the same contract as writeSync.

writer.cork(callback?)

Batches multiple writes. The write pointer is only published to the reader when the cork is released, reducing atomic operation overhead.

When called with a callback, uncork is called automatically when the callback returns. When called without a callback, you must call uncork() manually.

writer.uncork()

Decrements the cork counter. When it reaches zero, publishes the pending write position to the reader. Safe to call when not corked (no-op).

writer.flushSync()

Immediately publishes the pending write position to the reader. Unlike uncork, this does not interact with the cork counter — it forces a flush regardless.

Benchmarks

Measured on AMD EPYC 9355P (4.29 GHz), Node.js 25.6.1, 8 MiB ring buffer, Docker (x64-linux).

Each benchmark writes batches of fixed-size messages from the main thread and reads them in a worker thread. The shared ring buffer is compared against Node.js postMessage (structured clone).

Throughput

Sizeshared (buffer)shared (string)postMessage (buffer)postMessage (string)
64 B838 MiB/s388 MiB/s24 MiB/s42 MiB/s
256 B2.65 GiB/s1.46 GiB/s89 MiB/s168 MiB/s
1 KiB4.95 GiB/s4.86 GiB/s339 MiB/s525 MiB/s
4 KiB8.42 GiB/s15.11 GiB/s1.12 GiB/s1.86 GiB/s
16 KiB12.02 GiB/s33.27 GiB/s4.12 GiB/s6.02 GiB/s
64 KiB12.96 GiB/s43.66 GiB/s9.33 GiB/s14.73 GiB/s

Message rate

Sizeshared (buffer)shared (string)postMessage (buffer)postMessage (string)
64 B13.73 M/s6.35 M/s391 K/s693 K/s
256 B11.14 M/s6.14 M/s366 K/s689 K/s
1 KiB5.19 M/s5.09 M/s348 K/s538 K/s
4 KiB2.21 M/s3.96 M/s295 K/s488 K/s
16 KiB788 K/s2.18 M/s270 K/s395 K/s
64 KiB212 K/s715 K/s153 K/s241 K/s

Key findings

  • Small messages (64–256 B): The shared ring buffer with Buffer.set delivers 13.7–11.1 M msg/s — up to 35x faster than postMessage (buffer) and 20x faster than postMessage (string). Per-message overhead dominates at these sizes, and avoiding structured cloning makes the biggest difference.

  • Medium messages (1 KiB): Buffer.set and string are nearly identical (4.95 vs 4.86 GiB/s), both ~9x faster than the best postMessage variant.

  • Large messages (4–64 KiB): Shared string overtakes Buffer.set and scales to 43.7 GiB/s at 64 KiB — 3.4x faster than Buffer.set and 3.0x faster than postMessage (string). At every size, the shared ring buffer outperforms postMessage.

  • Caveat: The string benchmark uses ASCII-only content. Multi-byte UTF-8 strings will not hit V8's vectorized fast path and will be significantly slower.

Running the benchmark

node --allow-natives-syntax packages/shared/src/bench.mjs

License

MIT

FAQs

Package last updated on 15 Mar 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts